diff options
Diffstat (limited to 'app')
1969 files changed, 18530 insertions, 11217 deletions
diff --git a/app/assets/images/board-intro.svg b/app/assets/images/board-intro.svg new file mode 100644 index 00000000000..8f04077d598 --- /dev/null +++ b/app/assets/images/board-intro.svg @@ -0,0 +1 @@ +<svg width="566" height="213" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5 .5h166c2.467 0 4.5 2.084 4.5 4.695v202.61c0 2.611-2.033 4.695-4.5 4.695H5c-2.467 0-4.5-2.084-4.5-4.695V5.195C.5 2.584 2.533.5 5 .5z" fill="#F2F2F2" stroke="#DFDFDF"/><g filter="url(#filter0_i)"><path d="M366 0H200c-2.761 0-5 2.326-5 5.195v202.61c0 2.869 2.239 5.195 5 5.195h166c2.761 0 5-2.326 5-5.195V5.195C371 2.325 368.761 0 366 0z" fill="#F2F2F2"/></g><path d="M200 .5h166c2.467 0 4.5 2.084 4.5 4.695v202.61c0 2.611-2.033 4.695-4.5 4.695H200c-2.467 0-4.5-2.084-4.5-4.695V5.195C195.5 2.584 197.533.5 200 .5z" stroke="#DFDFDF"/><path d="M395 .5h166c2.467 0 4.5 2.084 4.5 4.695v202.61c0 2.611-2.033 4.695-4.5 4.695H395c-2.467 0-4.5-2.084-4.5-4.695V5.195C390.5 2.584 392.533.5 395 .5z" fill="#F2F2F2" stroke="#DFDFDF"/><rect x="210" y="15.585" width="40" height="13.507" rx="6.754" fill="#D9534F"/><path d="M17.73 20.826c0-.456.047-.897.14-1.323.103-.427.261-.805.476-1.135.215-.34.495-.606.84-.8.346-.204.765-.306 1.26-.306s.915.102 1.26.306c.346.194.625.46.84.8.215.33.369.708.462 1.135.103.426.154.867.154 1.323 0 .437-.051.863-.154 1.28a3.397 3.397 0 01-.462 1.106c-.215.33-.494.596-.84.8-.345.194-.765.29-1.26.29s-.915-.096-1.26-.29a2.507 2.507 0 01-.84-.8 3.64 3.64 0 01-.476-1.106 5.845 5.845 0 01-.14-1.28zm-2.198 0c0 .757.112 1.465.336 2.124.224.65.546 1.217.966 1.702.42.485.933.868 1.54 1.149.616.272 1.307.407 2.072.407.775 0 1.465-.135 2.072-.407a4.528 4.528 0 001.54-1.15c.42-.484.742-1.051.966-1.701a6.56 6.56 0 00.336-2.124c0-.776-.112-1.493-.336-2.153a5.228 5.228 0 00-.966-1.745 4.444 4.444 0 00-1.54-1.164c-.607-.281-1.297-.422-2.072-.422-.765 0-1.456.14-2.072.422-.607.281-1.12.67-1.54 1.164a5.228 5.228 0 00-.966 1.745 6.655 6.655 0 00-.336 2.153zm14.775 3.797c-.318 0-.588-.068-.812-.204a1.694 1.694 0 01-.546-.524 2.674 2.674 0 01-.294-.77 4.761 4.761 0 010-1.79c.056-.29.149-.548.28-.77.14-.224.317-.403.532-.539.224-.145.499-.218.826-.218.317 0 .583.073.798.218.224.136.406.32.546.553.14.223.238.48.294.77.065.292.098.588.098.888 0 .3-.028.597-.084.888-.056.29-.154.547-.294.77-.131.214-.308.388-.532.524-.215.136-.486.204-.812.204zm-3.654-6.168V28.61h1.988v-3.564h.028c.242.368.55.65.924.843.382.184.798.277 1.246.277.532 0 .994-.107 1.386-.32.401-.214.732-.5.994-.859.27-.358.471-.77.602-1.236.13-.465.196-.95.196-1.455 0-.533-.066-1.042-.196-1.527a3.803 3.803 0 00-.602-1.295 3.065 3.065 0 00-1.022-.887c-.411-.223-.901-.334-1.47-.334-.448 0-.859.092-1.232.276-.374.184-.682.48-.924.887h-.028v-.96h-1.89zm13.43 2.953H36.85c.01-.145.038-.31.084-.495.056-.184.145-.358.266-.523.13-.165.299-.3.504-.407.215-.117.48-.175.798-.175.486 0 .845.136 1.078.407.243.272.41.67.504 1.193zm-3.234 1.31h5.222a5.512 5.512 0 00-.14-1.674 4.097 4.097 0 00-.644-1.425 3.121 3.121 0 00-1.12-.99c-.457-.252-.994-.378-1.61-.378-.55 0-1.054.102-1.512.306a3.528 3.528 0 00-1.162.844 3.63 3.63 0 00-.756 1.25 4.539 4.539 0 00-.266 1.572c0 .582.084 1.115.252 1.6.178.485.425.902.742 1.25.318.35.705.621 1.162.815.458.184.97.277 1.54.277.822 0 1.522-.194 2.1-.582.579-.388 1.008-1.033 1.288-1.935h-1.75c-.065.233-.242.456-.532.67-.29.203-.634.305-1.036.305-.56 0-.99-.15-1.288-.451-.298-.3-.462-.786-.49-1.455zm6.401-4.263v7.52h1.988v-3.942c0-.766.122-1.314.364-1.643.243-.34.635-.51 1.176-.51.476 0 .808.156.994.466.187.3.28.762.28 1.382v4.248h1.988V21.35a5.85 5.85 0 00-.126-1.266 2.34 2.34 0 00-.406-.974 1.868 1.868 0 00-.812-.626c-.336-.155-.77-.233-1.302-.233-.42 0-.83.102-1.232.306-.401.194-.728.51-.98.946h-.042v-1.048h-1.89z" fill="#2E2E2E"/><g filter="url(#filter1_d)"><path d="M352 46.756H214c-2.209 0-4 1.86-4 4.156v30.132c0 2.295 1.791 4.156 4 4.156h138c2.209 0 4-1.86 4-4.156V50.912c0-2.295-1.791-4.156-4-4.156z" fill="#fff"/><path d="M352 46.756H214c-2.209 0-4 1.86-4 4.156v30.132c0 2.295 1.791 4.156 4 4.156h138c2.209 0 4-1.86 4-4.156V50.912c0-2.295-1.791-4.156-4-4.156z" stroke="#D6D6D6"/></g><path d="M332 57.146H222v6.234h110v-6.234zM300 68.576h-78v6.234h78v-6.234z" fill="#D8D8D8"/><g filter="url(#filter2_d)"><path d="M157 98.707H19c-2.21 0-4 1.861-4 4.156v30.132c0 2.295 1.79 4.156 4 4.156h138c2.209 0 4-1.861 4-4.156v-30.132c0-2.295-1.791-4.156-4-4.156z" fill="#fff"/><path d="M157 98.707H19c-2.21 0-4 1.861-4 4.156v30.132c0 2.295 1.79 4.156 4 4.156h138c2.209 0 4-1.861 4-4.156v-30.132c0-2.295-1.791-4.156-4-4.156z" stroke="#D6D6D6"/></g><path d="M137 109.098H27v6.234h110v-6.234zM105 120.527H27v6.234h78v-6.234z" fill="#D8D8D8"/><g filter="url(#filter3_d)"><path d="M352 96.63H214c-2.209 0-4 1.86-4 4.155v30.132c0 2.295 1.791 4.156 4 4.156h138c2.209 0 4-1.861 4-4.156v-30.132c0-2.295-1.791-4.156-4-4.156z" fill="#fff"/><path d="M352 96.63H214c-2.209 0-4 1.86-4 4.155v30.132c0 2.295 1.791 4.156 4 4.156h138c2.209 0 4-1.861 4-4.156v-30.132c0-2.295-1.791-4.156-4-4.156z" stroke="#D6D6D6"/></g><path d="M316 107.02h-94v6.234h94v-6.234zM292 118.449h-70v6.234h70v-6.234z" fill="#D8D8D8"/><g filter="url(#filter4_d)"><path d="M157 46.756H19c-2.21 0-4 1.86-4 4.156v30.132c0 2.295 1.79 4.156 4 4.156h138c2.209 0 4-1.86 4-4.156V50.912c0-2.295-1.791-4.156-4-4.156z" fill="#fff"/><path d="M157 46.756H19c-2.21 0-4 1.86-4 4.156v30.132c0 2.295 1.79 4.156 4 4.156h138c2.209 0 4-1.86 4-4.156V50.912c0-2.295-1.791-4.156-4-4.156z" stroke="#D6D6D6"/></g><path d="M121 57.146H27v6.234h94v-6.234zM97 68.576H27v6.234h70v-6.234z" fill="#D8D8D8"/><g filter="url(#filter5_d)"><path d="M157 148.581H19c-2.21 0-4 1.86-4 4.156v30.131c0 2.296 1.79 4.156 4 4.156h138c2.209 0 4-1.86 4-4.156v-30.131c0-2.296-1.791-4.156-4-4.156z" fill="#fff"/><path d="M157 148.581H19c-2.21 0-4 1.86-4 4.156v30.131c0 2.296 1.79 4.156 4 4.156h138c2.209 0 4-1.86 4-4.156v-30.131c0-2.296-1.791-4.156-4-4.156z" stroke="#D6D6D6"/></g><path d="M145 158.971H27v6.234h118v-6.234zM121 170.4H27v6.234h94V170.4z" fill="#D8D8D8"/><path d="M547 46.756H409c-2.209 0-4 1.86-4 4.156v30.132c0 2.295 1.791 4.156 4 4.156h138c2.209 0 4-1.86 4-4.156V50.912c0-2.295-1.791-4.156-4-4.156z" fill="#fff" stroke="#D6D6D6" stroke-dasharray="4 2"/><path d="M414.63 19.08h2.128a4.299 4.299 0 00-.476-1.6 3.726 3.726 0 00-.966-1.163 4.003 4.003 0 00-1.316-.728 4.796 4.796 0 00-1.554-.247c-.765 0-1.456.14-2.072.422-.607.281-1.12.67-1.54 1.164a5.228 5.228 0 00-.966 1.745 6.655 6.655 0 00-.336 2.153c0 .757.112 1.465.336 2.124.224.65.546 1.217.966 1.702.42.485.933.868 1.54 1.149.616.272 1.307.407 2.072.407.616 0 1.181-.097 1.694-.29a3.909 3.909 0 001.344-.844c.383-.369.691-.815.924-1.339.233-.523.378-1.11.434-1.76h-2.128c-.084.699-.317 1.261-.7 1.688-.373.427-.896.64-1.568.64-.495 0-.915-.097-1.26-.291a2.512 2.512 0 01-.84-.8 3.64 3.64 0 01-.476-1.106 5.863 5.863 0 01-.14-1.28c0-.456.047-.897.14-1.323.103-.427.261-.805.476-1.135.215-.34.495-.606.84-.8.345-.204.765-.306 1.26-.306.271 0 .527.049.77.146.252.087.476.213.672.378.196.165.359.359.49.582.131.213.215.45.252.713zm3.559-3.49v10.386h1.988V15.59h-1.988zm5.317 6.633c0-.3.028-.597.084-.888.056-.29.15-.548.28-.77a1.64 1.64 0 01.546-.539c.224-.145.504-.218.84-.218.336 0 .616.073.84.218.234.136.416.315.546.538.14.223.238.48.294.771a4.684 4.684 0 010 1.775 2.387 2.387 0 01-.294.77c-.13.224-.312.403-.546.54-.224.135-.504.203-.84.203-.336 0-.616-.068-.84-.204a1.64 1.64 0 01-.546-.538 2.62 2.62 0 01-.28-.771 4.685 4.685 0 01-.084-.887zm-1.988 0c0 .6.089 1.144.266 1.629.178.485.43.902.756 1.25.327.34.719.602 1.176.786.458.184.971.277 1.54.277.57 0 1.083-.092 1.54-.277a3.307 3.307 0 001.19-.785c.327-.35.579-.766.756-1.251a4.701 4.701 0 00.266-1.63c0-.6-.088-1.144-.266-1.629a3.45 3.45 0 00-.756-1.25 3.22 3.22 0 00-1.19-.8 3.904 3.904 0 00-1.54-.291c-.569 0-1.082.096-1.54.29a3.259 3.259 0 00-1.176.8c-.326.34-.578.757-.756 1.252a4.725 4.725 0 00-.266 1.629zm10.323 1.309h-1.89c.019.504.126.926.322 1.265.205.33.462.597.77.8.317.204.677.35 1.078.437a5.777 5.777 0 002.436 0 2.909 2.909 0 001.064-.422c.308-.204.555-.47.742-.8.196-.34.294-.757.294-1.251 0-.35-.065-.64-.196-.873a1.747 1.747 0 00-.518-.596 2.611 2.611 0 00-.742-.393 7.729 7.729 0 00-.84-.247c-.28-.068-.555-.131-.826-.19a8.052 8.052 0 01-.728-.189 1.609 1.609 0 01-.504-.29.575.575 0 01-.196-.451.54.54 0 01.112-.364.724.724 0 01.266-.218c.112-.049.233-.078.364-.087.131-.02.252-.03.364-.03.355 0 .663.073.924.219.261.135.406.402.434.8h1.89c-.037-.466-.154-.849-.35-1.15a2.225 2.225 0 00-.714-.741 2.95 2.95 0 00-.994-.393 5.617 5.617 0 00-2.282 0 2.885 2.885 0 00-1.008.378 2.046 2.046 0 00-.728.727c-.177.31-.266.708-.266 1.193 0 .33.065.611.196.844.131.223.303.412.518.567.215.146.457.267.728.364.28.087.565.165.854.233.709.155 1.26.31 1.652.465.401.155.602.388.602.698 0 .184-.042.34-.126.466a1.053 1.053 0 01-.322.29 1.658 1.658 0 01-.42.16 2.265 2.265 0 01-1.008-.028 1.546 1.546 0 01-.476-.219 1.383 1.383 0 01-.35-.392 1.266 1.266 0 01-.126-.582zm10.851-2.124h-3.234c.01-.145.038-.31.084-.495.056-.184.145-.358.266-.523.131-.165.299-.3.504-.407.215-.117.481-.175.798-.175.486 0 .845.136 1.078.407.243.272.411.67.504 1.193zm-3.234 1.31h5.222a5.478 5.478 0 00-.14-1.674 4.077 4.077 0 00-.644-1.425 3.112 3.112 0 00-1.12-.99c-.457-.252-.994-.378-1.61-.378-.55 0-1.054.102-1.512.306a3.532 3.532 0 00-1.162.844c-.326.349-.578.766-.756 1.25a4.548 4.548 0 00-.266 1.572c0 .582.084 1.115.252 1.6.178.485.425.902.742 1.25.318.35.705.621 1.162.815.458.184.971.277 1.54.277.822 0 1.522-.194 2.1-.582.579-.388 1.008-1.033 1.288-1.935h-1.75c-.065.233-.242.456-.532.67-.289.203-.634.305-1.036.305-.56 0-.989-.15-1.288-.451-.298-.3-.462-.786-.49-1.455zm11.54-.524c0 .31-.028.61-.084.901-.056.291-.15.553-.28.786a1.521 1.521 0 01-.532.538c-.215.136-.486.204-.812.204a1.51 1.51 0 01-.798-.204 2 2 0 01-.546-.553 2.622 2.622 0 01-.308-.785 3.933 3.933 0 01-.098-.873c0-.31.028-.606.084-.887.065-.291.163-.548.294-.771a1.64 1.64 0 01.546-.538c.224-.136.499-.204.826-.204.326 0 .597.068.812.204.214.135.387.315.518.538.14.213.238.465.294.756.056.281.084.577.084.888zm.028 2.822v.96h1.89V15.59h-1.988v3.782h-.028a2.106 2.106 0 00-.924-.83 2.66 2.66 0 00-1.218-.29c-.532 0-.999.111-1.4.334-.402.213-.738.5-1.008.858a4.032 4.032 0 00-.602 1.251c-.131.466-.196.95-.196 1.455 0 .524.065 1.028.196 1.513.14.485.34.916.602 1.294.27.369.611.665 1.022.888.41.213.886.32 1.428.32.476 0 .9-.088 1.274-.262.382-.184.69-.48.924-.887h.028z" fill="#2E2E2E"/><path d="M340.093 89.04a.976.976 0 00-1.398.22 1.065 1.065 0 00.212 1.452l1.186-1.673zm85.907.836l-11.413 1.82 7.224 9.36L426 89.876zm-85.395 2.116a.975.975 0 001.394-.243 1.066 1.066 0 00-.233-1.45l-1.161 1.693zm5.465 1.342a.978.978 0 00-1.382.312 1.064 1.064 0 00.3 1.436l1.082-1.748zm2.533 4.049a.98.98 0 001.367-.375 1.062 1.062 0 00-.36-1.42l-1.007 1.795zm5.555.775a.982.982 0 00-1.343.459c-.249.512-.052 1.137.441 1.396l.902-1.855zm2.941 3.746c.505.232 1.095-.005 1.319-.53.223-.525-.005-1.138-.51-1.37l-.809 1.9zm5.608.104c-.52-.195-1.093.084-1.281.624-.188.539.081 1.135.601 1.33l.68-1.954zm3.356 3.332c.529.163 1.086-.151 1.242-.702.157-.551-.146-1.128-.675-1.291l-.567 1.993zm5.57-.676c-.54-.118-1.071.241-1.185.802-.114.562.232 1.113.772 1.231l.413-2.033zm3.788 2.803c.547.081 1.053-.314 1.13-.882.078-.568-.303-1.094-.85-1.175l-.28 2.057zm5.405-1.537c-.552-.031-1.023.409-1.052.981-.029.574.394 1.063.946 1.094l.106-2.075zm4.164 2.15c.552-.011.991-.485.98-1.058-.011-.575-.467-1.031-1.019-1.02l.039 2.078zm5.093-2.424c-.549.063-.945.577-.884 1.147.061.571.556.981 1.105.918l-.221-2.065zm4.447 1.414c.543-.105.902-.647.801-1.211-.101-.564-.623-.937-1.166-.832l.365 2.043zm4.642-3.252c-.531.155-.842.728-.694 1.28.149.553.701.876 1.233.721l-.539-2.001zm4.601.664c.521-.192.793-.786.608-1.327a.991.991 0 00-1.277-.632l.669 1.959zm4.128-3.961c-.503.235-.728.85-.502 1.374a.986.986 0 001.322.522l-.82-1.896zm4.641-.036a1.06 1.06 0 00.421-1.403.982.982 0 00-1.35-.438l.929 1.841zm3.552-4.497c-.47.301-.616.941-.326 1.43a.98.98 0 001.377.338l-1.051-1.768zm4.602-.647c.454-.327.568-.974.254-1.446a.978.978 0 00-1.392-.263l1.138 1.709zm3.031-4.903a1.067 1.067 0 00-.171 1.459.975.975 0 001.404.177l-1.233-1.636zm-84.82.274c.527.404 1.094.832 1.698 1.28l1.161-1.692c-.594-.44-1.152-.862-1.673-1.26l-1.186 1.672zm6.081 4.37a96.649 96.649 0 003.615 2.3l1.007-1.795a93.481 93.481 0 01-3.54-2.253l-1.082 1.748zm8.268 4.931a80.434 80.434 0 003.843 1.891l.809-1.9a79.08 79.08 0 01-3.75-1.846l-.902 1.855zm8.771 3.949c1.309.492 2.655.953 4.036 1.378l.567-1.993a67.852 67.852 0 01-3.923-1.339l-.68 1.954zm9.193 2.735c1.373.302 2.773.56 4.201.77l.28-2.057a59.56 59.56 0 01-4.068-.746l-.413 2.033zm9.5 1.308c1.402.077 2.826.104 4.27.075l-.039-2.078a55.27 55.27 0 01-4.125-.072l-.106 2.075zm9.584-.284a56.47 56.47 0 004.226-.651l-.365-2.043a54.612 54.612 0 01-4.082.629l.221 2.065zm9.407-1.902a59.941 59.941 0 004.062-1.337l-.669-1.959a57.797 57.797 0 01-3.932 1.295l.539 2.001zm9.01-3.402a70.301 70.301 0 003.821-1.932l-.929-1.84a66.75 66.75 0 01-3.712 1.876l.82 1.896zm8.424-4.661a81.532 81.532 0 003.551-2.415l-1.138-1.71a79.562 79.562 0 01-3.464 2.357l1.051 1.768z" fill="#5398AD"/><path d="M216.724 19.523v6.984h1.884v-6.984h2.568V17.94h-7.02v1.584h2.568zm5.436 3.888c0-.248.024-.492.072-.732.048-.24.128-.452.24-.636.12-.184.276-.332.468-.444.192-.12.432-.18.72-.18s.528.06.72.18c.2.112.356.26.468.444.12.184.204.396.252.636a3.723 3.723 0 010 1.464 1.925 1.925 0 01-.252.636 1.25 1.25 0 01-.468.444c-.192.112-.432.168-.72.168s-.528-.056-.72-.168a1.382 1.382 0 01-.468-.444 2.095 2.095 0 01-.24-.636 3.723 3.723 0 01-.072-.732zm-1.704 0c0 .496.076.944.228 1.344.152.4.368.744.648 1.032.28.28.616.496 1.008.648.392.152.832.228 1.32.228.488 0 .928-.076 1.32-.228.4-.152.74-.368 1.02-.648.28-.288.496-.632.648-1.032.152-.4.228-.848.228-1.344a3.76 3.76 0 00-.228-1.344 2.836 2.836 0 00-.648-1.032 2.776 2.776 0 00-1.02-.66 3.465 3.465 0 00-1.32-.24c-.488 0-.928.08-1.32.24a2.836 2.836 0 00-1.656 1.692 3.76 3.76 0 00-.228 1.344zm15.272-.024c0 .256-.024.504-.072.744-.048.24-.128.456-.24.648a1.277 1.277 0 01-.456.444c-.184.112-.416.168-.696.168-.264 0-.492-.056-.684-.168a1.692 1.692 0 01-.468-.456 2.123 2.123 0 01-.264-.648 3.16 3.16 0 01-.084-.72c0-.256.024-.5.072-.732.056-.24.14-.452.252-.636.12-.184.276-.332.468-.444.192-.112.428-.168.708-.168.28 0 .512.056.696.168.184.112.332.26.444.444.12.176.204.384.252.624.048.232.072.476.072.732zm.024 2.328v.792h1.62V17.94h-1.704v3.12h-.024a1.784 1.784 0 00-.792-.684 2.35 2.35 0 00-1.044-.24c-.456 0-.856.092-1.2.276a2.57 2.57 0 00-.864.708c-.224.296-.396.64-.516 1.032a4.264 4.264 0 00-.168 1.2c0 .432.056.848.168 1.248.12.4.292.756.516 1.068.232.304.524.548.876.732.352.176.76.264 1.224.264.408 0 .772-.072 1.092-.216.328-.152.592-.396.792-.732h.024zm4.42-2.304c0-.248.024-.492.072-.732.048-.24.128-.452.24-.636.12-.184.276-.332.468-.444.192-.12.432-.18.72-.18s.528.06.72.18c.2.112.356.26.468.444.12.184.204.396.252.636a3.723 3.723 0 010 1.464 1.925 1.925 0 01-.252.636 1.25 1.25 0 01-.468.444c-.192.112-.432.168-.72.168s-.528-.056-.72-.168a1.382 1.382 0 01-.468-.444 2.095 2.095 0 01-.24-.636 3.723 3.723 0 01-.072-.732zm-1.704 0c0 .496.076.944.228 1.344.152.4.368.744.648 1.032.28.28.616.496 1.008.648.392.152.832.228 1.32.228.488 0 .928-.076 1.32-.228.4-.152.74-.368 1.02-.648.28-.288.496-.632.648-1.032.152-.4.228-.848.228-1.344a3.76 3.76 0 00-.228-1.344 2.836 2.836 0 00-.648-1.032 2.776 2.776 0 00-1.02-.66 3.465 3.465 0 00-1.32-.24c-.488 0-.928.08-1.32.24a2.836 2.836 0 00-1.656 1.692 3.76 3.76 0 00-.228 1.344z" fill="#fff"/><defs><filter id="filter0_i" x="195" y="0" width="176" height="213" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/><feOffset dy="3"/><feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/><feColorMatrix values="0 0 0 0 0.85098 0 0 0 0 0.32549 0 0 0 0 0.309804 0 0 0 1 0"/><feBlend in2="shape" result="effect1_innerShadow"/></filter><filter id="filter1_d" x="208.5" y="46.256" width="149" height="41.444" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="1"/><feGaussianBlur stdDeviation=".5"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0791084 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow"/><feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape"/></filter><filter id="filter2_d" x="13.5" y="98.207" width="149" height="41.444" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="1"/><feGaussianBlur stdDeviation=".5"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0791084 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow"/><feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape"/></filter><filter id="filter3_d" x="208.5" y="96.129" width="149" height="41.444" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="1"/><feGaussianBlur stdDeviation=".5"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0791084 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow"/><feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape"/></filter><filter id="filter4_d" x="13.5" y="46.256" width="149" height="41.444" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="1"/><feGaussianBlur stdDeviation=".5"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0791084 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow"/><feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape"/></filter><filter id="filter5_d" x="13.5" y="148.081" width="149" height="41.444" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="1"/><feGaussianBlur stdDeviation=".5"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0791084 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow"/><feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape"/></filter></defs></svg> diff --git a/app/assets/javascripts/access_tokens/index.js b/app/assets/javascripts/access_tokens/index.js index 319144193f1..b4353af30d5 100644 --- a/app/assets/javascripts/access_tokens/index.js +++ b/app/assets/javascripts/access_tokens/index.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import ExpiresAtField from './components/expires_at_field.vue'; -const getInputAttrs = el => { +const getInputAttrs = (el) => { const input = el.querySelector('input'); return { diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js index 05de970e387..6b9f46dcfb6 100644 --- a/app/assets/javascripts/activities.js +++ b/app/assets/javascripts/activities.js @@ -9,9 +9,9 @@ export default class Activities { constructor(container = '') { this.container = container; - Pager.init(20, true, false, data => data, this.updateTooltips, this.container); + Pager.init(20, true, false, (data) => data, this.updateTooltips, this.container); - $('.event-filter-link').on('click', e => { + $('.event-filter-link').on('click', (e) => { e.preventDefault(); this.toggleFilter(e.currentTarget); this.reloadActivities(); @@ -24,7 +24,7 @@ export default class Activities { reloadActivities() { $('.content_list').html(''); - Pager.init(20, true, false, data => data, this.updateTooltips, this.container); + Pager.init(20, true, false, (data) => data, this.updateTooltips, this.container); } toggleFilter(sender) { diff --git a/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue b/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue index cb9aa50fa68..c58ded3f1f5 100644 --- a/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue +++ b/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue @@ -61,14 +61,14 @@ export default { }, }, selectedCommitsCount() { - return this.selectedCommits.filter(selectedCommit => selectedCommit.isSelected).length; + return this.selectedCommits.filter((selectedCommit) => selectedCommit.isSelected).length; }, shouldPurge() { return this.selectedCommitsCount !== this.selectedCommits.length; }, uniqueCommits() { return this.selectedCommits.filter( - selectedCommit => + (selectedCommit) => selectedCommit.isSelected && findCommitIndex(this.contextCommits, selectedCommit.short_id) === -1, ); @@ -126,7 +126,7 @@ export default { this.focusSearch(); if (this.shouldPurge) { this.setSelectedCommits( - [...this.commits, ...this.selectedCommits].filter(commit => commit.isSelected), + [...this.commits, ...this.selectedCommits].filter((commit) => commit.isSelected), ); } } @@ -178,7 +178,7 @@ export default { this.setCommits({ commits: tempCommits }); this.setSelectedCommits([ ...tempSelectedCommits, - ...tempCommits.filter(commit => commit.isSelected), + ...tempCommits.filter((commit) => commit.isSelected), ]); }, handleCreateContextCommits() { @@ -186,7 +186,7 @@ export default { return Promise.all([ this.createContextCommits({ commits: this.uniqueCommits }), this.removeContextCommits(), - ]).then(values => { + ]).then((values) => { if (values[0] || values[1]) { window.location.reload(); } diff --git a/app/assets/javascripts/add_context_commits_modal/store/actions.js b/app/assets/javascripts/add_context_commits_modal/store/actions.js index d23955182b2..1bf54b159ee 100644 --- a/app/assets/javascripts/add_context_commits_modal/store/actions.js +++ b/app/assets/javascripts/add_context_commits_modal/store/actions.js @@ -27,10 +27,10 @@ export const searchCommits = ({ dispatch, commit, state }, searchText) => { return axios .get(state.contextCommitsPath, params) .then(({ data }) => { - let commits = data.map(o => ({ ...o, isSelected: false })); - commits = commits.map(c => { + let commits = data.map((o) => ({ ...o, isSelected: false })); + commits = commits.map((c) => { const isPresent = state.selectedCommits.find( - selectedCommit => selectedCommit.short_id === c.short_id && selectedCommit.isSelected, + (selectedCommit) => selectedCommit.short_id === c.short_id && selectedCommit.isSelected, ); if (isPresent) { return { ...c, isSelected: true }; @@ -50,7 +50,7 @@ export const searchCommits = ({ dispatch, commit, state }, searchText) => { export const setCommits = ({ commit }, { commits: data, silentAddition = false }) => { let commits = _.uniqBy(data, 'short_id'); - commits = _.orderBy(data, c => new Date(c.committed_date), ['desc']); + commits = _.orderBy(data, (c) => new Date(c.committed_date), ['desc']); if (silentAddition) { commit(types.SET_COMMITS_SILENT, commits); } else { @@ -60,7 +60,7 @@ export const setCommits = ({ commit }, { commits: data, silentAddition = false } export const createContextCommits = ({ state }, { commits, forceReload = false }) => Api.createContextCommits(state.projectId, state.mergeRequestIid, { - commits: commits.map(commit => commit.short_id), + commits: commits.map((commit) => commit.short_id), }) .then(() => { if (forceReload) { @@ -81,7 +81,7 @@ export const fetchContextCommits = ({ dispatch, commit, state }) => { commit(types.FETCH_CONTEXT_COMMITS); return Api.allContextCommits(state.projectId, state.mergeRequestIid) .then(({ data }) => { - const contextCommits = data.map(o => ({ ...o, isSelected: true })); + const contextCommits = data.map((o) => ({ ...o, isSelected: true })); dispatch('setContextCommits', contextCommits); dispatch('setCommits', { commits: [...state.commits, ...contextCommits], @@ -121,7 +121,7 @@ export const setSelectedCommits = ({ commit }, selected) => { let selectedCommits = _.uniqBy(selected, 'short_id'); selectedCommits = _.orderBy( selectedCommits, - selectedCommit => new Date(selectedCommit.committed_date), + (selectedCommit) => new Date(selectedCommit.committed_date), ['desc'], ); commit(types.SET_SELECTED_COMMITS, selectedCommits); diff --git a/app/assets/javascripts/add_context_commits_modal/utils.js b/app/assets/javascripts/add_context_commits_modal/utils.js index 3495ee17cd3..114913b1893 100644 --- a/app/assets/javascripts/add_context_commits_modal/utils.js +++ b/app/assets/javascripts/add_context_commits_modal/utils.js @@ -1,5 +1,5 @@ export const findCommitIndex = (commits, commitShortId) => { - return commits.findIndex(commit => commit.short_id === commitShortId); + return commits.findIndex((commit) => commit.short_id === commitShortId); }; export const setCommitStatus = (commits, commitIndex, selected) => { diff --git a/app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js b/app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js index f89533aeb1d..a357d5d2f1f 100644 --- a/app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js +++ b/app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js @@ -1,7 +1,7 @@ import PayloadPreviewer from '~/pages/admin/application_settings/payload_previewer'; export default () => { - Array.from(document.querySelectorAll('.js-payload-preview-trigger')).forEach(trigger => { + Array.from(document.querySelectorAll('.js-payload-preview-trigger')).forEach((trigger) => { new PayloadPreviewer(trigger).init(); }); }; diff --git a/app/assets/javascripts/admin/cohorts/components/usage_ping_disabled.vue b/app/assets/javascripts/admin/cohorts/components/usage_ping_disabled.vue index bc2d96832fa..5da38495010 100644 --- a/app/assets/javascripts/admin/cohorts/components/usage_ping_disabled.vue +++ b/app/assets/javascripts/admin/cohorts/components/usage_ping_disabled.vue @@ -36,7 +36,7 @@ export default { ) " > - <template #docLink="{content}"> + <template #docLink="{ content }"> <gl-link :href="docsLink" target="_blank">{{ content }}</gl-link> </template> <template #strong="{ content }" diff --git a/app/assets/javascripts/admin/dev_ops_report/components/usage_ping_disabled.vue b/app/assets/javascripts/admin/dev_ops_report/components/usage_ping_disabled.vue index 316827e1b07..c0ad814172d 100644 --- a/app/assets/javascripts/admin/dev_ops_report/components/usage_ping_disabled.vue +++ b/app/assets/javascripts/admin/dev_ops_report/components/usage_ping_disabled.vue @@ -35,7 +35,7 @@ export default { ) " > - <template #docLink="{content}"> + <template #docLink="{ content }"> <gl-link :href="docsLink" target="_blank">{{ content }}</gl-link> </template> </gl-sprintf> diff --git a/app/assets/javascripts/admin/statistics_panel/index.js b/app/assets/javascripts/admin/statistics_panel/index.js index 39112e3ddc0..8c49fffe630 100644 --- a/app/assets/javascripts/admin/statistics_panel/index.js +++ b/app/assets/javascripts/admin/statistics_panel/index.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import StatisticsPanelApp from './components/app.vue'; import createStore from './store'; -export default function(el) { +export default function (el) { if (!el) { return false; } diff --git a/app/assets/javascripts/admin/statistics_panel/store/actions.js b/app/assets/javascripts/admin/statistics_panel/store/actions.js index dd04e492388..149540c4222 100644 --- a/app/assets/javascripts/admin/statistics_panel/store/actions.js +++ b/app/assets/javascripts/admin/statistics_panel/store/actions.js @@ -13,7 +13,7 @@ export const fetchStatistics = ({ dispatch }) => { .then(({ data }) => { dispatch('receiveStatisticsSuccess', convertObjectPropsToCamelCase(data, { deep: true })); }) - .catch(error => dispatch('receiveStatisticsError', error)); + .catch((error) => dispatch('receiveStatisticsError', error)); }; export const receiveStatisticsSuccess = ({ commit }, statistics) => diff --git a/app/assets/javascripts/admin/statistics_panel/store/getters.js b/app/assets/javascripts/admin/statistics_panel/store/getters.js index 1c1868b5bca..7ad1b0445ac 100644 --- a/app/assets/javascripts/admin/statistics_panel/store/getters.js +++ b/app/assets/javascripts/admin/statistics_panel/store/getters.js @@ -3,8 +3,8 @@ * and returns an array of the following form: * [{ key: "forks", label: "Forks", value: 50 }] */ -export const getStatistics = state => labels => - Object.keys(labels).map(key => { +export const getStatistics = (state) => (labels) => + Object.keys(labels).map((key) => { const result = { key, label: labels[key], diff --git a/app/assets/javascripts/admin/users/components/user_avatar.vue b/app/assets/javascripts/admin/users/components/user_avatar.vue new file mode 100644 index 00000000000..4f79c4fd451 --- /dev/null +++ b/app/assets/javascripts/admin/users/components/user_avatar.vue @@ -0,0 +1,53 @@ +<script> +import { GlAvatarLink, GlAvatarLabeled, GlBadge } from '@gitlab/ui'; +import { USER_AVATAR_SIZE } from '../constants'; + +export default { + components: { + GlAvatarLink, + GlAvatarLabeled, + GlBadge, + }, + props: { + user: { + type: Object, + required: true, + }, + adminUserPath: { + type: String, + required: true, + }, + }, + computed: { + adminUserHref() { + return this.adminUserPath.replace('id', this.user.username); + }, + }, + USER_AVATAR_SIZE, +}; +</script> + +<template> + <gl-avatar-link + v-if="user" + class="js-user-link" + :href="adminUserHref" + :data-user-id="user.id" + :data-username="user.username" + > + <gl-avatar-labeled + :size="$options.USER_AVATAR_SIZE" + :src="user.avatarUrl" + :label="user.name" + :sub-label="user.email" + > + <template #meta> + <div v-for="(badge, idx) in user.badges" :key="idx" class="gl-p-1"> + <gl-badge class="gl-display-flex!" size="sm" :variant="badge.variant">{{ + badge.text + }}</gl-badge> + </div> + </template> + </gl-avatar-labeled> + </gl-avatar-link> +</template> diff --git a/app/assets/javascripts/admin/users/components/users_table.vue b/app/assets/javascripts/admin/users/components/users_table.vue index a2d68972519..15e31935a4c 100644 --- a/app/assets/javascripts/admin/users/components/users_table.vue +++ b/app/assets/javascripts/admin/users/components/users_table.vue @@ -1,14 +1,16 @@ <script> import { GlTable } from '@gitlab/ui'; import { __ } from '~/locale'; +import UserAvatar from './user_avatar.vue'; const DEFAULT_TH_CLASSES = 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!'; -const thWidthClass = width => `gl-w-${width}p ${DEFAULT_TH_CLASSES}`; +const thWidthClass = (width) => `gl-w-${width}p ${DEFAULT_TH_CLASSES}`; export default { components: { GlTable, + UserAvatar, }, props: { users: { @@ -58,6 +60,10 @@ export default { :empty-text="s__('AdminUsers|No users found')" show-empty stacked="md" - /> + > + <template #cell(name)="{ item: user }"> + <UserAvatar :user="user" :admin-user-path="paths.adminUser" /> + </template> + </gl-table> </div> </template> diff --git a/app/assets/javascripts/admin/users/constants.js b/app/assets/javascripts/admin/users/constants.js new file mode 100644 index 00000000000..675fcf00c39 --- /dev/null +++ b/app/assets/javascripts/admin/users/constants.js @@ -0,0 +1 @@ +export const USER_AVATAR_SIZE = 32; diff --git a/app/assets/javascripts/admin/users/index.js b/app/assets/javascripts/admin/users/index.js index 21780ee9984..f35b57c4e1a 100644 --- a/app/assets/javascripts/admin/users/index.js +++ b/app/assets/javascripts/admin/users/index.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import AdminUsersApp from './components/app.vue'; -export default function(el = document.querySelector('#js-admin-users-app')) { +export default function (el = document.querySelector('#js-admin-users-app')) { if (!el) { return false; } @@ -11,7 +11,7 @@ export default function(el = document.querySelector('#js-admin-users-app')) { return new Vue({ el, - render: createElement => + render: (createElement) => createElement(AdminUsersApp, { props: { users: convertObjectPropsToCamelCase(JSON.parse(users), { deep: true }), diff --git a/app/assets/javascripts/alert_handler.js b/app/assets/javascripts/alert_handler.js index 26b0142f6a2..3c867f196d6 100644 --- a/app/assets/javascripts/alert_handler.js +++ b/app/assets/javascripts/alert_handler.js @@ -8,9 +8,9 @@ export default function initAlertHandler() { const DISMISS_LABEL = '[aria-label="Dismiss"]'; const DISMISS_CLASS = '.gl-alert-dismiss'; - DISMISSIBLE_SELECTORS.forEach(selector => { + DISMISSIBLE_SELECTORS.forEach((selector) => { const elements = document.querySelectorAll(selector); - elements.forEach(element => { + elements.forEach((element) => { const button = element.querySelector(DISMISS_LABEL) || element.querySelector(DISMISS_CLASS); if (!button) { return; diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue index 1f3fdd5eef2..895c6e76019 100644 --- a/app/assets/javascripts/alert_management/components/alert_details.vue +++ b/app/assets/javascripts/alert_management/components/alert_details.vue @@ -140,7 +140,7 @@ export default { }, currentTabIndex: { get() { - return this.$options.tabsConfig.findIndex(tab => tab.id === this.activeTab); + return this.$options.tabsConfig.findIndex((tab) => tab.id === this.activeTab); }, set(tabIdx) { const tabId = this.$options.tabsConfig[tabIdx].id; @@ -194,15 +194,21 @@ export default { projectPath: this.projectPath, }, }) - .then(({ data: { createAlertIssue: { errors, issue } } }) => { - if (errors?.length) { - [this.createIncidentError] = errors; - this.incidentCreationInProgress = false; - } else if (issue) { - visitUrl(this.incidentPath(issue.iid)); - } - }) - .catch(error => { + .then( + ({ + data: { + createAlertIssue: { errors, issue }, + }, + }) => { + if (errors?.length) { + [this.createIncidentError] = errors; + this.incidentCreationInProgress = false; + } else if (issue) { + visitUrl(this.incidentPath(issue.iid)); + } + }, + ) + .catch((error) => { this.createIncidentError = error; this.incidentCreationInProgress = false; }); diff --git a/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue b/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue index c5ff2dc0d11..9b0e5090a75 100644 --- a/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue +++ b/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue @@ -6,20 +6,11 @@ import alertsHelpUrlQuery from '../graphql/queries/alert_help_url.query.graphql' export default { i18n: { emptyState: { - opsgenie: { - title: s__('AlertManagement|Opsgenie is enabled'), - info: s__( - 'AlertManagement|You have enabled the Opsgenie integration. Your alerts will be visible directly in Opsgenie.', - ), - buttonText: s__('AlertManagement|View alerts in Opsgenie'), - }, - gitlab: { - title: s__('AlertManagement|Surface alerts in GitLab'), - info: s__( - 'AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents.', - ), - buttonText: s__('AlertManagement|Authorize external service'), - }, + title: s__('AlertManagement|Surface alerts in GitLab'), + info: s__( + 'AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents.', + ), + buttonText: s__('AlertManagement|Authorize external service'), }, moreInformation: s__('AlertManagement|More information'), }, @@ -33,46 +24,27 @@ export default { query: alertsHelpUrlQuery, }, }, - inject: [ - 'enableAlertManagementPath', - 'userCanEnableAlertManagement', - 'emptyAlertSvgPath', - 'opsgenieMvcEnabled', - 'opsgenieMvcTargetUrl', - ], + inject: ['enableAlertManagementPath', 'userCanEnableAlertManagement', 'emptyAlertSvgPath'], data() { return { alertsHelpUrl: '', }; }, - computed: { - emptyState() { - return { - ...(this.opsgenieMvcEnabled - ? this.$options.i18n.emptyState.opsgenie - : this.$options.i18n.emptyState.gitlab), - link: this.opsgenieMvcEnabled ? this.opsgenieMvcTargetUrl : this.enableAlertManagementPath, - }; - }, - alertsCanBeEnabled() { - return this.userCanEnableAlertManagement || this.opsgenieMvcEnabled; - }, - }, }; </script> <template> <div> - <gl-empty-state :title="emptyState.title" :svg-path="emptyAlertSvgPath"> + <gl-empty-state :title="$options.i18n.emptyState.title" :svg-path="emptyAlertSvgPath"> <template #description> <div class="gl-display-block"> - <span>{{ emptyState.info }}</span> - <gl-link v-if="!opsgenieMvcEnabled" :href="alertsHelpUrl" target="_blank"> + <span>{{ $options.i18n.emptyState.info }}</span> + <gl-link :href="alertsHelpUrl" target="_blank"> {{ $options.i18n.moreInformation }} </gl-link> </div> - <div v-if="alertsCanBeEnabled" class="gl-display-block center gl-pt-4"> - <gl-button category="primary" variant="success" :href="emptyState.link"> - {{ emptyState.buttonText }} + <div v-if="userCanEnableAlertManagement" class="gl-display-block center gl-pt-4"> + <gl-button category="primary" variant="success" :href="enableAlertManagementPath"> + {{ $options.i18n.emptyState.buttonText }} </gl-button> </div> </template> diff --git a/app/assets/javascripts/alert_management/components/alert_management_table.vue b/app/assets/javascripts/alert_management/components/alert_management_table.vue index f287b425826..2bad15faa85 100644 --- a/app/assets/javascripts/alert_management/components/alert_management_table.vue +++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue @@ -23,7 +23,7 @@ import { } from '~/vue_shared/components/paginated_table_with_search_and_tabs/constants'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import { convertToSnakeCase } from '~/lib/utils/text_utility'; -import getAlerts from '../graphql/queries/get_alerts.query.graphql'; +import getAlertsQuery from '~/graphql_shared/queries/get_alerts.query.graphql'; import getAlertsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql'; import { ALERTS_STATUS_TABS, @@ -119,7 +119,7 @@ export default { apollo: { alerts: { fetchPolicy: fetchPolicies.CACHE_AND_NETWORK, - query: getAlerts, + query: getAlertsQuery, variables() { return { searchTerm: this.searchTerm, @@ -138,7 +138,7 @@ export default { data.project || {}; const now = new Date(); - const listWithData = list.map(alert => { + const listWithData = list.map((alert) => { const then = new Date(alert.startedAt); const diff = now - then; diff --git a/app/assets/javascripts/alert_management/components/alert_metrics.vue b/app/assets/javascripts/alert_management/components/alert_metrics.vue index 8a6490ecd5c..dd4faa03c00 100644 --- a/app/assets/javascripts/alert_management/components/alert_metrics.vue +++ b/app/assets/javascripts/alert_management/components/alert_metrics.vue @@ -33,7 +33,7 @@ export default { }); this.metricEmbedComponent = MetricEmbed; }) - .catch(e => Sentry.captureException(e)); + .catch((e) => Sentry.captureException(e)); } }, }; diff --git a/app/assets/javascripts/alert_management/components/alert_status.vue b/app/assets/javascripts/alert_management/components/alert_status.vue index 3083a85cbd9..2afdeb8b6fd 100644 --- a/app/assets/javascripts/alert_management/components/alert_status.vue +++ b/app/assets/javascripts/alert_management/components/alert_status.vue @@ -3,7 +3,7 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { s__ } from '~/locale'; import Tracking from '~/tracking'; import { trackAlertStatusUpdateOptions } from '../constants'; -import updateAlertStatusMutation from '../graphql/mutations/update_alert_status.mutation.graphql'; +import updateAlertStatusMutation from '~/graphql_shared/mutations/update_alert_status.mutation.graphql'; export default { i18n: { @@ -57,7 +57,7 @@ export default { projectPath: this.projectPath, }, }) - .then(resp => { + .then((resp) => { this.trackStatusUpdate(status); const errors = resp.data?.updateAlertStatus?.errors || []; diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue index 3af68d42ddf..2a999b908f9 100644 --- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue +++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue @@ -92,7 +92,7 @@ export default { }, sortedUsers() { return this.users - .map(user => ({ ...user, active: this.isActive(user.username) })) + .map((user) => ({ ...user, active: this.isActive(user.username) })) .sort((a, b) => (a.active === b.active ? 0 : a.active ? -1 : 1)); // eslint-disable-line no-nested-ternary }, dropdownClass() { @@ -192,7 +192,7 @@ export default { </script> <template> - <div class="block alert-assignees "> + <div class="block alert-assignees"> <div ref="assignees" class="sidebar-collapsed-icon" @click="$emit('toggle-sidebar')"> <gl-icon name="user" :size="14" /> <gl-loading-icon v-if="isUpdating" /> diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue index 84d54466a10..485395bcac2 100644 --- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue +++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue @@ -115,7 +115,7 @@ export default { variables: this.getAlertQueryVariables, }); - const data = produce(sourceData, draftData => { + const data = produce(sourceData, (draftData) => { // eslint-disable-next-line no-param-reassign draftData.project.alertManagementAlerts.nodes[0].todos.nodes = []; }); diff --git a/app/assets/javascripts/alert_management/details.js b/app/assets/javascripts/alert_management/details.js index cbbdecae390..4217b702d0a 100644 --- a/app/assets/javascripts/alert_management/details.js +++ b/app/assets/javascripts/alert_management/details.js @@ -9,7 +9,7 @@ import createRouter from './router'; Vue.use(VueApollo); -export default selector => { +export default (selector) => { const domEl = document.querySelector(selector); const { alertId, projectPath, projectIssuesPath, projectId } = domEl.dataset; const router = createRouter(); @@ -18,7 +18,7 @@ export default selector => { Mutation: { toggleSidebarStatus: (_, __, { cache }) => { const sourceData = cache.readQuery({ query: sidebarStatusQuery }); - const data = produce(sourceData, draftData => { + const data = produce(sourceData, (draftData) => { // eslint-disable-next-line no-param-reassign draftData.sidebarStatus = !draftData.sidebarStatus; }); @@ -30,7 +30,7 @@ export default selector => { const apolloProvider = new VueApollo({ defaultClient: createDefaultClient(resolvers, { cacheConfig: { - dataIdFromObject: object => { + dataIdFromObject: (object) => { // eslint-disable-next-line no-underscore-dangle if (object.__typename === 'AlertManagementAlert') { return object.iid; @@ -51,6 +51,9 @@ export default selector => { // eslint-disable-next-line no-new new Vue({ el: selector, + components: { + AlertDetails, + }, provide: { projectPath, alertId, @@ -58,9 +61,6 @@ export default selector => { projectId, }, apolloProvider, - components: { - AlertDetails, - }, router, render(createElement) { return createElement('alert-details', {}); diff --git a/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql b/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql index 406dfe97ce0..9a9ae369519 100644 --- a/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql +++ b/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql @@ -1,5 +1,5 @@ -#import "./list_item.fragment.graphql" -#import "./alert_note.fragment.graphql" +#import "~/graphql_shared/fragments/alert.fragment.graphql" +#import "~/graphql_shared/fragments/alert_note.fragment.graphql" fragment AlertDetailItem on AlertManagementAlert { ...AlertListItem diff --git a/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql b/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql index 5008bfa5e1b..63d952a4857 100644 --- a/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql +++ b/app/assets/javascripts/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql @@ -1,4 +1,4 @@ -#import "../fragments/alert_note.fragment.graphql" +#import "~/graphql_shared/fragments/alert_note.fragment.graphql" mutation alertSetAssignees($projectPath: ID!, $assigneeUsernames: [String!]!, $iid: String!) { alertSetAssignees( diff --git a/app/assets/javascripts/alert_management/list.js b/app/assets/javascripts/alert_management/list.js index e34450204fb..b484841ed2c 100644 --- a/app/assets/javascripts/alert_management/list.js +++ b/app/assets/javascripts/alert_management/list.js @@ -17,12 +17,10 @@ export default () => { emptyAlertSvgPath, populatingAlertsHelpUrl, alertsHelpUrl, - opsgenieMvcTargetUrl, textQuery, assigneeUsernameQuery, alertManagementEnabled, userCanEnableAlertManagement, - opsgenieMvcEnabled, } = domEl.dataset; const apolloProvider = new VueApollo({ @@ -30,7 +28,7 @@ export default () => { {}, { cacheConfig: { - dataIdFromObject: object => { + dataIdFromObject: (object) => { // eslint-disable-next-line no-underscore-dangle if (object.__typename === 'AlertManagementAlert') { return object.iid; @@ -50,6 +48,9 @@ export default () => { return new Vue({ el: selector, + components: { + AlertManagementList, + }, provide: { projectPath, textQuery, @@ -57,15 +58,10 @@ export default () => { enableAlertManagementPath, populatingAlertsHelpUrl, emptyAlertSvgPath, - opsgenieMvcTargetUrl, alertManagementEnabled: parseBoolean(alertManagementEnabled), userCanEnableAlertManagement: parseBoolean(userCanEnableAlertManagement), - opsgenieMvcEnabled: parseBoolean(opsgenieMvcEnabled), }, apolloProvider, - components: { - AlertManagementList, - }, render(createElement) { return createElement('alert-management-list'); }, diff --git a/app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue b/app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue index f2394ce385f..c0e93d315a4 100644 --- a/app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue +++ b/app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue @@ -91,25 +91,11 @@ export default { ]; }, }, - watch: { - activated() { - this.updateIcon(); - }, - }, methods: { - updateIcon() { - return document.querySelectorAll('.js-service-active-status').forEach(icon => { - if (icon.dataset.value === this.activated.toString()) { - icon.classList.remove('d-none'); - } else { - icon.classList.add('d-none'); - } - }); - }, resetKey() { return axios .put(this.formPath, { service: { token: '' } }) - .then(res => { + .then((res) => { this.authorizationKey = res.data.token; }) .catch(() => { diff --git a/app/assets/javascripts/alerts_service_settings/index.js b/app/assets/javascripts/alerts_service_settings/index.js index fe83ced2ee7..ddba966ffb3 100644 --- a/app/assets/javascripts/alerts_service_settings/index.js +++ b/app/assets/javascripts/alerts_service_settings/index.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import { parseBoolean } from '~/lib/utils/common_utils'; import AlertsServiceForm from './components/alerts_service_form.vue'; -export default el => { +export default (el) => { if (!el) { return null; } diff --git a/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue index f6474efcc1f..c52e9f5c264 100644 --- a/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue +++ b/app/assets/javascripts/alerts_settings/components/alert_mapping_builder.vue @@ -40,6 +40,11 @@ export default { directives: { GlTooltip, }, + inject: { + gitlabAlertFields: { + default: gitlabFieldsMock, + }, + }, props: { payloadFields: { type: Array, @@ -57,16 +62,11 @@ export default { gitlabFields: this.gitlabAlertFields, }; }, - inject: { - gitlabAlertFields: { - default: gitlabFieldsMock, - }, - }, computed: { mappingData() { - return this.gitlabFields.map(gitlabField => { + return this.gitlabFields.map((gitlabField) => { const mappingFields = this.payloadFields.filter(({ type }) => - type.some(t => gitlabField.compatibleTypes.includes(t)), + type.some((t) => gitlabField.compatibleTypes.includes(t)), ); const foundMapping = this.mapping.find( @@ -88,26 +88,26 @@ export default { }, methods: { setMapping(gitlabKey, mappingKey, valueKey) { - const fieldIndex = this.gitlabFields.findIndex(field => field.name === gitlabKey); + const fieldIndex = this.gitlabFields.findIndex((field) => field.name === gitlabKey); const updatedField = { ...this.gitlabFields[fieldIndex], ...{ [valueKey]: mappingKey } }; Vue.set(this.gitlabFields, fieldIndex, updatedField); }, setSearchTerm(search = '', searchFieldKey, gitlabKey) { - const fieldIndex = this.gitlabFields.findIndex(field => field.name === gitlabKey); + const fieldIndex = this.gitlabFields.findIndex((field) => field.name === gitlabKey); const updatedField = { ...this.gitlabFields[fieldIndex], ...{ [searchFieldKey]: search } }; Vue.set(this.gitlabFields, fieldIndex, updatedField); }, filterFields(searchTerm = '', fields) { const search = searchTerm.toLowerCase(); - return fields.filter(field => field.label.toLowerCase().includes(search)); + return fields.filter((field) => field.label.toLowerCase().includes(search)); }, isSelected(fieldValue, mapping) { return fieldValue === mapping; }, selectedValue(name) { return ( - this.payloadFields.find(item => item.name === name)?.label || + this.payloadFields.find((item) => item.name === name)?.label || this.$options.i18n.makeSelection ); }, diff --git a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue index cf16750dbf8..6cfb4601192 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue @@ -97,8 +97,8 @@ export default { }; }, mounted() { - const callback = entries => { - const isVisible = entries.some(entry => entry.isIntersecting); + const callback = (entries) => { + const isVisible = entries.some((entry) => entry.isIntersecting); if (isVisible) { this.trackPageViews(); diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue index b2be563522a..1ae7f826ce6 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue @@ -18,14 +18,11 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import MappingBuilder from './alert_mapping_builder.vue'; import AlertSettingsFormHelpBlock from './alert_settings_form_help_block.vue'; import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql'; -import service from '../services'; import { - integrationTypesNew, + integrationTypes, JSON_VALIDATE_DELAY, targetPrometheusUrlPlaceholder, - targetOpsgenieUrlPlaceholder, typeSet, - sectionHash, } from '../constants'; // Mocks will be removed when integrating with BE is ready // data format is defined and will be the same as mocked (maybe with some minor changes) @@ -91,20 +88,13 @@ export const i18n = { 'AlertSettings|Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.', ), }, - // TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657 - opsgenie: { - label: s__('AlertSettings|2. Add link to your Opsgenie alert list'), - info: s__( - 'AlertSettings|Utilizing this option will link the GitLab Alerts navigation item to your existing Opsgenie instance. By selecting this option, you cannot receive alerts from any other source in GitLab; it will effectively be turning Alerts within GitLab off as a feature.', - ), - }, }, }; export default { + integrationTypes, placeholders: { prometheus: targetPrometheusUrlPlaceholder, - opsgenie: targetOpsgenieUrlPlaceholder, }, JSON_VALIDATE_DELAY, typeSet, @@ -127,6 +117,7 @@ export default { directives: { GlModal: GlModalDirective, }, + mixins: [glFeatureFlagsMixin()], inject: { generic: { default: {}, @@ -134,12 +125,7 @@ export default { prometheus: { default: {}, }, - // TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657 - opsgenie: { - default: {}, - }, }, - mixins: [glFeatureFlagsMixin()], props: { loading: { type: Boolean, @@ -149,12 +135,6 @@ export default { type: Boolean, required: true, }, - // TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657 - canManageOpsgenie: { - type: Boolean, - required: false, - default: false, - }, }, apollo: { currentIntegration: { @@ -163,7 +143,7 @@ export default { }, data() { return { - selectedIntegration: integrationTypesNew[0].value, + selectedIntegration: integrationTypes[0].value, active: false, formVisible: false, integrationTestPayload: { @@ -174,8 +154,6 @@ export default { customMapping: null, parsingPayload: false, currentIntegration: null, - // TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657 - isManagingOpsgenie: false, }; }, computed: { @@ -185,32 +163,12 @@ export default { jsonIsValid() { return this.integrationTestPayload.error === null; }, - // TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657 - disabledIntegrations() { - const options = []; - if (this.opsgenie.active) { - options.push(typeSet.http, typeSet.prometheus); - } else if (!this.canManageOpsgenie) { - options.push(typeSet.opsgenie); - } - - return options; - }, - options() { - return integrationTypesNew.map(el => ({ - ...el, - disabled: this.disabledIntegrations.includes(el.value), - })); - }, selectedIntegrationType() { switch (this.selectedIntegration) { case typeSet.http: return this.generic; case typeSet.prometheus: return this.prometheus; - // TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657 - case typeSet.opsgenie: - return this.opsgenie; default: return {}; } @@ -285,49 +243,17 @@ export default { }, methods: { integrationTypeSelect() { - if (this.selectedIntegration === integrationTypesNew[0].value) { + if (this.selectedIntegration === integrationTypes[0].value) { this.formVisible = false; } else { this.formVisible = true; } - - // TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657 - if (this.canManageOpsgenie && this.selectedIntegration === typeSet.opsgenie) { - this.isManagingOpsgenie = true; - this.active = this.opsgenie.active; - this.integrationForm.apiUrl = this.opsgenie.opsgenieMvcTargetUrl; - } else { - // TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657 - this.isManagingOpsgenie = false; - } - }, - // TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657 - submitWithOpsgenie() { - return service - .updateGenericActive({ - endpoint: this.opsgenie.formPath, - params: { - service: { - opsgenie_mvc_target_url: this.integrationForm.apiUrl, - opsgenie_mvc_enabled: this.active, - }, - }, - }) - .then(() => { - window.location.hash = sectionHash; - window.location.reload(); - }); }, submitWithTestPayload() { this.$emit('set-test-alert-payload', this.testAlertPayload); this.submit(); }, submit() { - // TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657 - if (this.isManagingOpsgenie) { - return this.submitWithOpsgenie(); - } - const { name, apiUrl } = this.integrationForm; const variables = this.selectedIntegration === typeSet.http @@ -343,7 +269,7 @@ export default { return this.$emit('create-new-integration', integrationPayload); }, reset() { - this.selectedIntegration = integrationTypesNew[0].value; + this.selectedIntegration = integrationTypes[0].value; this.integrationTypeSelect(); if (this.currentIntegration) { @@ -360,9 +286,6 @@ export default { error: null, }; this.active = false; - - // TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657 - this.isManagingOpsgenie = false; }, resetAuthKey() { if (!this.currentIntegration) { @@ -390,10 +313,10 @@ export default { // TODO: replace with real BE mutation when ready; this.parsingPayload = true; - return new Promise(resolve => { + return new Promise((resolve) => { setTimeout(() => resolve(mockedCustomMapping), 1000); }) - .then(res => { + .then((res) => { const mapping = { ...res }; delete mapping.storedMapping; this.customMapping = res; @@ -408,7 +331,7 @@ export default { }, getIntegrationMapping() { // TODO: replace with real BE mutation when ready; - return Promise.resolve(mockedCustomMapping).then(res => { + return Promise.resolve(mockedCustomMapping).then((res) => { this.customMapping = res; this.integrationTestPayload.json = res?.samplePayload.body; }); @@ -428,8 +351,8 @@ export default { <gl-form-select v-model="selectedIntegration" :disabled="isSelectDisabled" - :class="{ 'gl-bg-gray-100!': isSelectDisabled }" - :options="options" + class="mw-100" + :options="$options.integrationTypes" @change="integrationTypeSelect" /> @@ -441,37 +364,7 @@ export default { </div> </gl-form-group> <gl-collapse v-model="formVisible" class="gl-mt-3"> - <!-- TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657 --> - <div v-if="isManagingOpsgenie"> - <gl-form-group - id="integration-webhook" - :label="$options.i18n.integrationFormSteps.opsgenie.label" - label-for="integration-webhook" - > - <span class="gl-my-4"> - {{ $options.i18n.integrationFormSteps.opsgenie.info }} - </span> - - <gl-toggle - v-model="active" - :is-loading="loading" - :label="__('Active')" - class="gl-my-4 gl-font-weight-normal" - /> - - <gl-form-input - id="opsgenie-opsgenieMvcTargetUrl" - v-model="integrationForm.apiUrl" - type="text" - :placeholder="$options.placeholders.opsgenie" - /> - - <span class="gl-text-gray-400 gl-my-1"> - {{ $options.i18n.integrationFormSteps.prometheusFormUrl.help }} - </span> - </gl-form-group> - </div> - <div v-else> + <div> <gl-form-group id="name-integration" :label="$options.i18n.integrationFormSteps.step2.label" @@ -661,9 +554,7 @@ export default { data-testid="integration-form-submit" >{{ s__('AlertSettings|Save integration') }} </gl-button> - <!-- TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657 --> <gl-button - v-if="!isManagingOpsgenie" data-testid="integration-test-and-submit" :disabled="isSubmitTestPayloadDisabled" category="secondary" @@ -672,12 +563,7 @@ export default { @click="submitWithTestPayload" >{{ s__('AlertSettings|Save and test payload') }}</gl-button > - <gl-button - type="reset" - class="js-no-auto-disable" - :class="{ 'gl-ml-3': isManagingOpsgenie }" - >{{ __('Cancel') }}</gl-button - > + <gl-button type="reset" class="js-no-auto-disable">{{ __('Cancel') }}</gl-button> </div> </gl-collapse> </gl-form> diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue index a55e63c3bc0..d0cac066ffa 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue @@ -1,5 +1,4 @@ <script> -import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui'; import { s__ } from '~/locale'; import { fetchPolicies } from '~/lib/graphql'; import createFlash, { FLASH_TYPES } from '~/flash'; @@ -41,10 +40,6 @@ export default { ), }, components: { - // TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657 - GlAlert, - GlLink, - GlSprintf, IntegrationsList, AlertSettingsForm, }, @@ -55,10 +50,6 @@ export default { prometheus: { default: {}, }, - // TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657 - opsgenie: { - default: {}, - }, projectPath: { default: '', }, @@ -105,13 +96,6 @@ export default { canAddIntegration() { return this.multiIntegrations || this.integrations?.list?.length < 2; }, - canManageOpsgenie() { - return ( - this.opsgenie.active || - this.integrations?.list?.every(({ active }) => active === false) || - this.integrations?.list?.length === 0 - ); - }, }, methods: { createNewIntegration({ type, variables }) { @@ -243,7 +227,9 @@ export default { }); }, editIntegration({ id }) { - const currentIntegration = this.integrations.list.find(integration => integration.id === id); + const currentIntegration = this.integrations.list.find( + (integration) => integration.id === id, + ); this.$apollo.mutate({ mutation: updateCurrentIntergrationMutation, variables: { @@ -317,27 +303,7 @@ export default { <template> <div> - <!-- TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657 --> - <gl-alert v-if="opsgenie.active" :dismissible="false" variant="tip"> - <gl-sprintf - :message=" - s__( - 'AlertSettings|We will soon be introducing the ability to create multiple unique HTTP endpoints. When this functionality is live, you will be able to configure an integration with Opsgenie to surface Opsgenie alerts in GitLab. This will replace the current Opsgenie integration which will be deprecated. %{linkStart}More Information%{linkEnd}', - ) - " - > - <template #link="{ content }"> - <gl-link - class="gl-display-inline-block" - href="https://gitlab.com/gitlab-org/gitlab/-/issues/273657" - target="_blank" - >{{ content }}</gl-link - > - </template> - </gl-sprintf> - </gl-alert> <integrations-list - v-else :integrations="integrations.list" :loading="loading" @edit-integration="editIntegration" @@ -346,7 +312,6 @@ export default { <alert-settings-form :loading="isUpdating" :can-add-integration="canAddIntegration" - :can-manage-opsgenie="canManageOpsgenie" @create-new-integration="createNewIntegration" @update-integration="updateIntegration" @reset-token="resetToken" diff --git a/app/assets/javascripts/alerts_settings/constants.js b/app/assets/javascripts/alerts_settings/constants.js index e30dc2ad553..ecd7c921b2f 100644 --- a/app/assets/javascripts/alerts_settings/constants.js +++ b/app/assets/javascripts/alerts_settings/constants.js @@ -40,22 +40,15 @@ export const i18n = { integration: s__('AlertSettings|Integration'), }; -// TODO: Delete as part of old form removal in 13.6 export const integrationTypes = [ + { value: '', text: s__('AlertSettings|Select integration type') }, { value: 'HTTP', text: s__('AlertSettings|HTTP Endpoint') }, { value: 'PROMETHEUS', text: s__('AlertSettings|External Prometheus') }, - { value: 'OPSGENIE', text: s__('AlertSettings|Opsgenie') }, -]; - -export const integrationTypesNew = [ - { value: '', text: s__('AlertSettings|Select integration type') }, - ...integrationTypes, ]; export const typeSet = { http: 'HTTP', prometheus: 'PROMETHEUS', - opsgenie: 'OPSGENIE', }; export const integrationToDeleteDefault = { id: null, name: '' }; @@ -63,7 +56,6 @@ export const integrationToDeleteDefault = { id: null, name: '' }; export const JSON_VALIDATE_DELAY = 250; export const targetPrometheusUrlPlaceholder = 'http://prometheus.example.com/'; -export const targetOpsgenieUrlPlaceholder = 'https://app.opsgenie.com/alert/list/'; export const sectionHash = 'js-alert-management-settings'; diff --git a/app/assets/javascripts/alerts_settings/graphql.js b/app/assets/javascripts/alerts_settings/graphql.js index 02c2def87fa..3dbfa69a8e9 100644 --- a/app/assets/javascripts/alerts_settings/graphql.js +++ b/app/assets/javascripts/alerts_settings/graphql.js @@ -14,7 +14,7 @@ const resolvers = { { cache }, ) => { const sourceData = cache.readQuery({ query: getCurrentIntegrationQuery }); - const data = produce(sourceData, draftData => { + const data = produce(sourceData, (draftData) => { if (id === null) { // eslint-disable-next-line no-param-reassign draftData.currentIntegration = null; diff --git a/app/assets/javascripts/alerts_settings/index.js b/app/assets/javascripts/alerts_settings/index.js index 41b19a675c5..85858956987 100644 --- a/app/assets/javascripts/alerts_settings/index.js +++ b/app/assets/javascripts/alerts_settings/index.js @@ -11,7 +11,7 @@ apolloProvider.clients.defaultClient.cache.writeData({ }); Vue.use(GlToast); -export default el => { +export default (el) => { if (!el) { return null; } @@ -29,16 +29,15 @@ export default el => { formPath, authorizationKey, url, - opsgenieMvcAvailable, - opsgenieMvcFormPath, - opsgenieMvcEnabled, - opsgenieMvcTargetUrl, projectPath, multiIntegrations, } = el.dataset; return new Vue({ el, + components: { + AlertSettingsWrapper, + }, provide: { prometheus: { active: parseBoolean(prometheusActivated), @@ -56,19 +55,10 @@ export default el => { token: authorizationKey, url, }, - opsgenie: { - formPath: opsgenieMvcFormPath, - active: parseBoolean(opsgenieMvcEnabled), - opsgenieMvcTargetUrl, - opsgenieMvcIsAvailable: parseBoolean(opsgenieMvcAvailable), - }, projectPath, multiIntegrations: parseBoolean(multiIntegrations), }, apolloProvider, - components: { - AlertSettingsWrapper, - }, render(createElement) { return createElement('alert-settings-wrapper'); }, diff --git a/app/assets/javascripts/alerts_settings/utils/cache_updates.js b/app/assets/javascripts/alerts_settings/utils/cache_updates.js index 18054b29fe9..758f3eb6dd4 100644 --- a/app/assets/javascripts/alerts_settings/utils/cache_updates.js +++ b/app/assets/javascripts/alerts_settings/utils/cache_updates.js @@ -14,7 +14,7 @@ const deleteIntegrationFromStore = (store, query, { httpIntegrationDestroy }, va variables, }); - const data = produce(sourceData, draftData => { + const data = produce(sourceData, (draftData) => { // eslint-disable-next-line no-param-reassign draftData.project.alertManagementIntegrations.nodes = draftData.project.alertManagementIntegrations.nodes.filter( ({ id }) => id !== integration.id, @@ -45,7 +45,7 @@ const addIntegrationToStore = ( variables, }); - const data = produce(sourceData, draftData => { + const data = produce(sourceData, (draftData) => { // eslint-disable-next-line no-param-reassign draftData.project.alertManagementIntegrations.nodes = [ integration, diff --git a/app/assets/javascripts/analytics/instance_statistics/components/instance_statistics_count_chart.vue b/app/assets/javascripts/analytics/instance_statistics/components/instance_statistics_count_chart.vue index a9bd1bb2f41..620f38bd50f 100644 --- a/app/assets/javascripts/analytics/instance_statistics/components/instance_statistics_count_chart.vue +++ b/app/assets/javascripts/analytics/instance_statistics/components/instance_statistics_count_chart.vue @@ -60,13 +60,13 @@ export default { return Object.values(this.errors); }, isLoading() { - return some(this.$apollo.queries, query => query?.loading); + return some(this.$apollo.queries, (query) => query?.loading); }, allQueriesFailed() { - return every(this.errorMessages, message => message.length); + return every(this.errorMessages, (message) => message.length); }, hasLoadingErrors() { - return some(this.errorMessages, message => message.length); + return some(this.errorMessages, (message) => message.length); }, errorMessage() { // show the generic loading message if all requests fail @@ -179,7 +179,7 @@ export default { }; }, }) - .catch(error => this.handleError({ identifier, error, message: errorMessage })); + .catch((error) => this.handleError({ identifier, error, message: errorMessage })); }, }, }; diff --git a/app/assets/javascripts/analytics/instance_statistics/components/projects_and_groups_chart.vue b/app/assets/javascripts/analytics/instance_statistics/components/projects_and_groups_chart.vue index e8e35c22fe1..46cc05fc124 100644 --- a/app/assets/javascripts/analytics/instance_statistics/components/projects_and_groups_chart.vue +++ b/app/assets/javascripts/analytics/instance_statistics/components/projects_and_groups_chart.vue @@ -11,7 +11,7 @@ import latestGroupsQuery from '../graphql/queries/groups.query.graphql'; import latestProjectsQuery from '../graphql/queries/projects.query.graphql'; import { getAverageByMonth } from '../utils'; -const sortByDate = data => sortBy(data, item => new Date(item[0]).getTime()); +const sortByDate = (data) => sortBy(data, (item) => new Date(item[0]).getTime()); const averageAndSortData = (data = [], maxDataPoints) => { const averaged = getAverageByMonth( @@ -148,7 +148,7 @@ export default { name: this.$options.i18n.xAxisTitle, type: 'category', axisLabel: { - formatter: value => { + formatter: (value) => { return formatDateAsMonth(value); }, }, @@ -189,7 +189,7 @@ export default { .fetchMore({ variables: { first: this.totalDataPoints, after: pageInfo.endCursor }, updateQuery: (previousResult, { fetchMoreResult }) => { - const results = produce(fetchMoreResult, newData => { + const results = produce(fetchMoreResult, (newData) => { // eslint-disable-next-line no-param-reassign newData[dataKey].nodes = [ ...previousResult[dataKey].nodes, @@ -199,7 +199,7 @@ export default { return results; }, }) - .catch(error => { + .catch((error) => { this.handleError({ error, message: errorMessage, dataKey }); }); } diff --git a/app/assets/javascripts/analytics/instance_statistics/components/users_chart.vue b/app/assets/javascripts/analytics/instance_statistics/components/users_chart.vue index a4a1d40b70b..03462113630 100644 --- a/app/assets/javascripts/analytics/instance_statistics/components/users_chart.vue +++ b/app/assets/javascripts/analytics/instance_statistics/components/users_chart.vue @@ -10,7 +10,7 @@ import { formatDateAsMonth } from '~/lib/utils/datetime_utility'; import usersQuery from '../graphql/queries/users.query.graphql'; import { getAverageByMonth } from '../utils'; -const sortByDate = data => sortBy(data, item => new Date(item[0]).getTime()); +const sortByDate = (data) => sortBy(data, (item) => new Date(item[0]).getTime()); export default { name: 'UsersChart', @@ -106,7 +106,7 @@ export default { .fetchMore({ variables: { first: this.totalDataPoints, after: this.pageInfo.endCursor }, updateQuery: (previousResult, { fetchMoreResult }) => { - return produce(fetchMoreResult, newUsers => { + return produce(fetchMoreResult, (newUsers) => { // eslint-disable-next-line no-param-reassign newUsers.users.nodes = [...previousResult.users.nodes, ...newUsers.users.nodes]; }); diff --git a/app/assets/javascripts/analytics/instance_statistics/utils.js b/app/assets/javascripts/analytics/instance_statistics/utils.js index e1fa5d155a2..396962ffad6 100644 --- a/app/assets/javascripts/analytics/instance_statistics/utils.js +++ b/app/assets/javascripts/analytics/instance_statistics/utils.js @@ -29,7 +29,7 @@ export function getAverageByMonth(items = [], options = {}) { return { ...memo, [month]: { sum: count, recordCount: 1 } }; }, {}); - return Object.keys(itemsMap).map(month => { + return Object.keys(itemsMap).map((month) => { const { sum, recordCount } = itemsMap[month]; const avg = sum / recordCount; if (shouldRound) { diff --git a/app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js b/app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js index d1f4b537b11..91cb48e181b 100644 --- a/app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js +++ b/app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js @@ -8,18 +8,18 @@ export default () => { return false; } - return containers.forEach(container => { + return containers.forEach((container) => { const { chartData } = container.dataset; const formattedData = JSON.parse(chartData); return new Vue({ el: container, - provide: { - formattedData, - }, components: { ActivityChart, }, + provide: { + formattedData, + }, render(createElement) { return createElement('activity-chart'); }, diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 8daccae3467..0a3db8ad3a6 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -5,6 +5,12 @@ import { __ } from '~/locale'; const DEFAULT_PER_PAGE = 20; +/** + * Slow deprecation Notice: Please rather use for new calls + * or during refactors /rest_api as this is doing named exports + * which support treeshaking + */ + const Api = { DEFAULT_PER_PAGE, groupsPath: '/api/:version/groups.json', @@ -13,6 +19,7 @@ const Api = { groupMilestonesPath: '/api/:version/groups/:id/milestones', subgroupsPath: '/api/:version/groups/:id/subgroups', namespacesPath: '/api/:version/namespaces.json', + groupInvitationsPath: '/api/:version/groups/:id/invitations', groupPackagesPath: '/api/:version/groups/:id/packages', projectPackagesPath: '/api/:version/projects/:id/packages', projectPackagePath: '/api/:version/projects/:id/packages/:package_id', @@ -23,6 +30,7 @@ const Api = { projectLabelsPath: '/:namespace_path/:project_path/-/labels', projectFileSchemaPath: '/:namespace_path/:project_path/-/schema/:ref/:filename', projectUsersPath: '/api/:version/projects/:id/users', + projectInvitationsPath: '/api/:version/projects/:id/invitations', projectMembersPath: '/api/:version/projects/:id/members', projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests', projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid', @@ -127,12 +135,18 @@ const Api = { }); }, - inviteGroupMember(id, data) { + addGroupMembersByUserId(id, data) { const url = Api.buildUrl(this.groupMembersPath).replace(':id', encodeURIComponent(id)); return axios.post(url, data); }, + inviteGroupMembersByEmail(id, data) { + const url = Api.buildUrl(this.groupInvitationsPath).replace(':id', encodeURIComponent(id)); + + return axios.post(url, data); + }, + groupMilestones(id, options) { const url = Api.buildUrl(this.groupMilestonesPath).replace(':id', encodeURIComponent(id)); @@ -144,7 +158,10 @@ const Api = { }); }, - // Return groups list. Filtered by query + /** + * @deprecated This method will be removed soon. Use the + * `getGroups` method in `~/rest_api` instead. + */ groups(query, options, callback = () => {}) { const url = Api.buildUrl(Api.groupsPath); return axios @@ -180,7 +197,10 @@ const Api = { .then(({ data }) => callback(data)); }, - // Return projects list. Filtered by query + /** + * @deprecated This method will be removed soon. Use the + * `getProjects` method in `~/rest_api` instead. + */ projects(query, options, callback = () => {}) { const url = Api.buildUrl(Api.projectsPath); const defaults = { @@ -217,12 +237,18 @@ const Api = { .then(({ data }) => data); }, - inviteProjectMembers(id, data) { + addProjectMembersByUserId(id, data) { const url = Api.buildUrl(this.projectMembersPath).replace(':id', encodeURIComponent(id)); return axios.post(url, data); }, + inviteProjectMembersByEmail(id, data) { + const url = Api.buildUrl(this.projectInvitationsPath).replace(':id', encodeURIComponent(id)); + + return axios.post(url, data); + }, + // Return single project project(projectPath) { const url = Api.buildUrl(Api.projectPath).replace(':id', encodeURIComponent(projectPath)); @@ -374,8 +400,8 @@ const Api = { .post(url, { label: data, }) - .then(res => callback(res.data)) - .catch(e => callback(e.response.data)); + .then((res) => callback(res.data)) + .catch((e) => callback(e.response.data)); }, // Return group projects list. Filtered by query @@ -389,10 +415,12 @@ const Api = { .get(url, { params: { ...defaults, ...options }, }) - .then(({ data }) => callback(data)) + .then(({ data }) => (callback ? callback(data) : data)) .catch(() => { flash(__('Something went wrong while fetching projects')); - callback(); + if (callback) { + callback(); + } }); }, @@ -414,10 +442,10 @@ const Api = { }); }, - applySuggestion(id) { + applySuggestion(id, message) { const url = Api.buildUrl(Api.applySuggestionPath).replace(':id', encodeURIComponent(id)); - return axios.put(url); + return axios.put(url, { commit_message: message }); }, applySuggestionBatch(ids) { @@ -429,7 +457,7 @@ const Api = { commitPipelines(projectId, sha) { const encodedProjectId = projectId .split('/') - .map(fragment => encodeURIComponent(fragment)) + .map((fragment) => encodeURIComponent(fragment)) .join('/'); const url = Api.buildUrl(Api.commitPipelinesPath) @@ -453,7 +481,7 @@ const Api = { .replace(':type', type) .replace(':key', encodeURIComponent(key)); - return axios.get(url, { params: options }).then(res => { + return axios.get(url, { params: options }).then((res) => { if (callback) callback(res.data); return res; @@ -465,7 +493,7 @@ const Api = { .replace(':id', encodeURIComponent(id)) .replace(':type', type); - return axios.get(url, { params }).then(res => { + return axios.get(url, { params }).then((res) => { if (callback) callback(res.data); return res; @@ -505,6 +533,10 @@ const Api = { .replace(':namespace_path', namespacePath); }, + /** + * @deprecated This method will be removed soon. Use the + * `getUsers` method in `~/rest_api` instead. + */ users(query, options) { const url = Api.buildUrl(this.usersPath); return axios.get(url, { @@ -516,6 +548,10 @@ const Api = { }); }, + /** + * @deprecated This method will be removed soon. Use the + * `getUser` method in `~/rest_api` instead. + */ user(id, options) { const url = Api.buildUrl(this.userPath).replace(':id', encodeURIComponent(id)); return axios.get(url, { @@ -523,11 +559,19 @@ const Api = { }); }, + /** + * @deprecated This method will be removed soon. Use the + * `getUserCounts` method in `~/rest_api` instead. + */ userCounts() { const url = Api.buildUrl(this.userCountsPath); return axios.get(url); }, + /** + * @deprecated This method will be removed soon. Use the + * `getUserStatus` method in `~/rest_api` instead. + */ userStatus(id, options) { const url = Api.buildUrl(this.userStatusPath).replace(':id', encodeURIComponent(id)); return axios.get(url, { @@ -535,6 +579,10 @@ const Api = { }); }, + /** + * @deprecated This method will be removed soon. Use the + * `getUserProjects` method in `~/rest_api` instead. + */ userProjects(userId, query, options, callback) { const url = Api.buildUrl(Api.userProjectsPath).replace(':id', userId); const defaults = { @@ -570,6 +618,10 @@ const Api = { }); }, + /** + * @deprecated This method will be removed soon. Use the + * `updateUserStatus` method in `~/rest_api` instead. + */ postUserStatus({ emoji, message, availability }) { const url = Api.buildUrl(this.userPostStatusPath); @@ -834,11 +886,18 @@ const Api = { page: 1, }; + const passedOptions = options; + + // calling search API with empty string will not return results + if (!passedOptions.search) { + passedOptions.search = undefined; + } + return axios .get(url, { params: { ...defaults, - ...options, + ...passedOptions, }, }) .then(({ data, headers }) => { diff --git a/app/assets/javascripts/api/api_utils.js b/app/assets/javascripts/api/api_utils.js new file mode 100644 index 00000000000..42eb5e7aaf3 --- /dev/null +++ b/app/assets/javascripts/api/api_utils.js @@ -0,0 +1,5 @@ +import { joinPaths } from '../lib/utils/url_utility'; + +export function buildApiUrl(url) { + return joinPaths('/', gon.relative_url_root || '', url.replace(':version', gon.api_version)); +} diff --git a/app/assets/javascripts/api/constants.js b/app/assets/javascripts/api/constants.js new file mode 100644 index 00000000000..b6c720a85f3 --- /dev/null +++ b/app/assets/javascripts/api/constants.js @@ -0,0 +1 @@ +export const DEFAULT_PER_PAGE = 20; diff --git a/app/assets/javascripts/api/groups_api.js b/app/assets/javascripts/api/groups_api.js new file mode 100644 index 00000000000..d4ba46656e6 --- /dev/null +++ b/app/assets/javascripts/api/groups_api.js @@ -0,0 +1,22 @@ +import axios from '../lib/utils/axios_utils'; +import { buildApiUrl } from './api_utils'; +import { DEFAULT_PER_PAGE } from './constants'; + +const GROUPS_PATH = '/api/:version/groups.json'; + +export function getGroups(query, options, callback = () => {}) { + const url = buildApiUrl(GROUPS_PATH); + return axios + .get(url, { + params: { + search: query, + per_page: DEFAULT_PER_PAGE, + ...options, + }, + }) + .then(({ data }) => { + callback(data); + + return data; + }); +} diff --git a/app/assets/javascripts/api/projects_api.js b/app/assets/javascripts/api/projects_api.js new file mode 100644 index 00000000000..d9a2467cff3 --- /dev/null +++ b/app/assets/javascripts/api/projects_api.js @@ -0,0 +1,27 @@ +import axios from '../lib/utils/axios_utils'; +import { buildApiUrl } from './api_utils'; +import { DEFAULT_PER_PAGE } from './constants'; + +const PROJECTS_PATH = '/api/:version/projects.json'; + +export function getProjects(query, options, callback = () => {}) { + const url = buildApiUrl(PROJECTS_PATH); + const defaults = { + search: query, + per_page: DEFAULT_PER_PAGE, + simple: true, + }; + + if (gon.current_user_id) { + defaults.membership = true; + } + + return axios + .get(url, { + params: Object.assign(defaults, options), + }) + .then(({ data, headers }) => { + callback(data); + return { data, headers }; + }); +} diff --git a/app/assets/javascripts/api/user_api.js b/app/assets/javascripts/api/user_api.js new file mode 100644 index 00000000000..e5983ec3c58 --- /dev/null +++ b/app/assets/javascripts/api/user_api.js @@ -0,0 +1,66 @@ +import axios from '../lib/utils/axios_utils'; +import { buildApiUrl } from './api_utils'; +import { DEFAULT_PER_PAGE } from './constants'; +import { deprecatedCreateFlash as flash } from '~/flash'; +import { __ } from '~/locale'; + +const USER_COUNTS_PATH = '/api/:version/user_counts'; +const USERS_PATH = '/api/:version/users.json'; +const USER_PATH = '/api/:version/users/:id'; +const USER_STATUS_PATH = '/api/:version/users/:id/status'; +const USER_PROJECTS_PATH = '/api/:version/users/:id/projects'; +const USER_POST_STATUS_PATH = '/api/:version/user/status'; + +export function getUsers(query, options) { + const url = buildApiUrl(USERS_PATH); + return axios.get(url, { + params: { + search: query, + per_page: DEFAULT_PER_PAGE, + ...options, + }, + }); +} + +export function getUser(id, options) { + const url = buildApiUrl(USER_PATH).replace(':id', encodeURIComponent(id)); + return axios.get(url, { + params: options, + }); +} + +export function getUserCounts() { + const url = buildApiUrl(USER_COUNTS_PATH); + return axios.get(url); +} + +export function getUserStatus(id, options) { + const url = buildApiUrl(USER_STATUS_PATH).replace(':id', encodeURIComponent(id)); + return axios.get(url, { + params: options, + }); +} + +export function getUserProjects(userId, query, options, callback) { + const url = buildApiUrl(USER_PROJECTS_PATH).replace(':id', userId); + const defaults = { + search: query, + per_page: DEFAULT_PER_PAGE, + }; + return axios + .get(url, { + params: { ...defaults, ...options }, + }) + .then(({ data }) => callback(data)) + .catch(() => flash(__('Something went wrong while fetching projects'))); +} + +export function updateUserStatus({ emoji, message, availability }) { + const url = buildApiUrl(USER_POST_STATUS_PATH); + + return axios.put(url, { + emoji, + message, + availability, + }); +} diff --git a/app/assets/javascripts/artifacts_settings/graphql/mutations/update_keep_latest_artifact_project_setting.mutation.graphql b/app/assets/javascripts/artifacts_settings/graphql/mutations/update_keep_latest_artifact_project_setting.mutation.graphql new file mode 100644 index 00000000000..d50fd665c16 --- /dev/null +++ b/app/assets/javascripts/artifacts_settings/graphql/mutations/update_keep_latest_artifact_project_setting.mutation.graphql @@ -0,0 +1,5 @@ +mutation updateKeepLatestArtifactProjectSetting($fullPath: ID!, $keepLatestArtifact: Boolean!) { + ciCdSettingsUpdate(input: { fullPath: $fullPath, keepLatestArtifact: $keepLatestArtifact }) { + errors + } +} diff --git a/app/assets/javascripts/artifacts_settings/graphql/queries/get_keep_latest_artifact_project_setting.query.graphql b/app/assets/javascripts/artifacts_settings/graphql/queries/get_keep_latest_artifact_project_setting.query.graphql new file mode 100644 index 00000000000..7486512c57c --- /dev/null +++ b/app/assets/javascripts/artifacts_settings/graphql/queries/get_keep_latest_artifact_project_setting.query.graphql @@ -0,0 +1,7 @@ +query getKeepLatestArtifactProjectSetting($fullPath: ID!) { + project(fullPath: $fullPath) { + ciCdSettings { + keepLatestArtifact + } + } +} diff --git a/app/assets/javascripts/artifacts_settings/index.js b/app/assets/javascripts/artifacts_settings/index.js new file mode 100644 index 00000000000..d99d2be81cf --- /dev/null +++ b/app/assets/javascripts/artifacts_settings/index.js @@ -0,0 +1,32 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import KeepLatestArtifactCheckbox from '~/artifacts_settings/keep_latest_artifact_checkbox.vue'; + +Vue.use(VueApollo); + +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), +}); + +export default (containerId = 'js-artifacts-settings-app') => { + const containerEl = document.getElementById(containerId); + + if (!containerEl) { + return false; + } + + const { fullPath, helpPagePath } = containerEl.dataset; + + return new Vue({ + el: containerEl, + apolloProvider, + provide: { + fullPath, + helpPagePath, + }, + render(createElement) { + return createElement(KeepLatestArtifactCheckbox); + }, + }); +}; diff --git a/app/assets/javascripts/artifacts_settings/keep_latest_artifact_checkbox.vue b/app/assets/javascripts/artifacts_settings/keep_latest_artifact_checkbox.vue new file mode 100644 index 00000000000..5684033f3af --- /dev/null +++ b/app/assets/javascripts/artifacts_settings/keep_latest_artifact_checkbox.vue @@ -0,0 +1,99 @@ +<script> +import { GlAlert, GlFormCheckbox, GlLink } from '@gitlab/ui'; +import { __ } from '~/locale'; +import GetKeepLatestArtifactProjectSetting from './graphql/queries/get_keep_latest_artifact_project_setting.query.graphql'; +import UpdateKeepLatestArtifactProjectSetting from './graphql/mutations/update_keep_latest_artifact_project_setting.mutation.graphql'; + +const FETCH_ERROR = __('There was a problem fetching the keep latest artifact setting.'); +const UPDATE_ERROR = __('There was a problem updating the keep latest artifact setting.'); + +export default { + components: { + GlAlert, + GlFormCheckbox, + GlLink, + }, + inject: { + fullPath: { + default: '', + }, + helpPagePath: { + default: '', + }, + }, + apollo: { + keepLatestArtifact: { + query: GetKeepLatestArtifactProjectSetting, + variables() { + return { + fullPath: this.fullPath, + }; + }, + update(data) { + return data.project?.ciCdSettings?.keepLatestArtifact; + }, + error() { + this.reportError(FETCH_ERROR); + }, + }, + }, + data() { + return { + keepLatestArtifact: true, + errorMessage: '', + isAlertDismissed: false, + }; + }, + computed: { + shouldShowAlert() { + return this.errorMessage && !this.isAlertDismissed; + }, + }, + methods: { + reportError(error) { + this.errorMessage = error; + this.isAlertDismissed = false; + }, + async updateSetting(checked) { + try { + const { data } = await this.$apollo.mutate({ + mutation: UpdateKeepLatestArtifactProjectSetting, + variables: { + fullPath: this.fullPath, + keepLatestArtifact: checked, + }, + }); + + if (data.ciCdSettingsUpdate.errors.length) { + this.reportError(UPDATE_ERROR); + } + } catch (error) { + this.reportError(UPDATE_ERROR); + } + }, + }, +}; +</script> + +<template> + <div> + <gl-alert + v-if="shouldShowAlert" + class="gl-mb-5" + variant="danger" + @dismiss="isAlertDismissed = true" + >{{ errorMessage }}</gl-alert + > + <gl-form-checkbox v-model="keepLatestArtifact" @change="updateSetting" + ><b class="gl-mr-3">{{ __('Keep artifacts from most recent successful jobs') }}</b> + <gl-link :href="helpPagePath">{{ __('More information') }}</gl-link></gl-form-checkbox + > + <p> + {{ + __( + 'The latest artifacts created by jobs in the most recent successful pipeline will be stored.', + ) + }} + </p> + </div> +</template> diff --git a/app/assets/javascripts/authentication/u2f/authenticate.js b/app/assets/javascripts/authentication/u2f/authenticate.js index f9b5ca3e5b4..f5217e9c9be 100644 --- a/app/assets/javascripts/authentication/u2f/authenticate.js +++ b/app/assets/javascripts/authentication/u2f/authenticate.js @@ -37,7 +37,7 @@ export default class U2FAuthenticate { // Note: The server library fixes this behaviour in (unreleased) version 1.0.0. // This can be removed once we upgrade. // https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4 - this.signRequests = u2fParams.sign_requests.map(request => omit(request, 'challenge')); + this.signRequests = u2fParams.sign_requests.map((request) => omit(request, 'challenge')); this.templates = { inProgress: '#js-authenticate-token-2fa-in-progress', @@ -48,7 +48,7 @@ export default class U2FAuthenticate { start() { return importU2FLibrary() - .then(utils => { + .then((utils) => { this.u2fUtils = utils; this.renderInProgress(); }) @@ -60,7 +60,7 @@ export default class U2FAuthenticate { this.appId, this.challenge, this.signRequests, - response => { + (response) => { if (response.errorCode) { const error = new U2FError(response.errorCode, 'authenticate'); return this.renderError(error); diff --git a/app/assets/javascripts/authentication/u2f/register.js b/app/assets/javascripts/authentication/u2f/register.js index 9773a9185f8..52940e1c305 100644 --- a/app/assets/javascripts/authentication/u2f/register.js +++ b/app/assets/javascripts/authentication/u2f/register.js @@ -34,7 +34,7 @@ export default class U2FRegister { start() { return importU2FLibrary() - .then(utils => { + .then((utils) => { this.u2fUtils = utils; this.renderSetup(); }) @@ -46,7 +46,7 @@ export default class U2FRegister { this.appId, this.registerRequests, this.signRequests, - response => { + (response) => { if (response.errorCode) { const error = new U2FError(response.errorCode, 'register'); return this.renderError(error); diff --git a/app/assets/javascripts/authentication/webauthn/authenticate.js b/app/assets/javascripts/authentication/webauthn/authenticate.js index 42c4c2b63bd..47cb7a40f76 100644 --- a/app/assets/javascripts/authentication/webauthn/authenticate.js +++ b/app/assets/javascripts/authentication/webauthn/authenticate.js @@ -39,11 +39,11 @@ export default class WebAuthnAuthenticate { authenticate() { navigator.credentials .get({ publicKey: this.webauthnParams }) - .then(resp => { + .then((resp) => { const convertedResponse = convertGetResponse(resp); this.renderAuthenticated(JSON.stringify(convertedResponse)); }) - .catch(err => { + .catch((err) => { this.flow.renderError(new WebAuthnError(err, 'authenticate')); }); } diff --git a/app/assets/javascripts/authentication/webauthn/register.js b/app/assets/javascripts/authentication/webauthn/register.js index 06e4ffd6f3e..62ebf85abe4 100644 --- a/app/assets/javascripts/authentication/webauthn/register.js +++ b/app/assets/javascripts/authentication/webauthn/register.js @@ -39,8 +39,8 @@ export default class WebAuthnRegister { .create({ publicKey: this.webauthnOptions, }) - .then(cred => this.renderRegistered(JSON.stringify(convertCreateResponse(cred)))) - .catch(err => this.flow.renderError(new WebAuthnError(err, 'register'))); + .then((cred) => this.renderRegistered(JSON.stringify(convertCreateResponse(cred)))) + .catch((err) => this.flow.renderError(new WebAuthnError(err, 'register'))); } renderSetup() { diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index d937060536a..22717a3f84c 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -56,13 +56,13 @@ export class AwardsHandler { } }, ); - this.registerEventListener('on', $parentEl, 'click', this.toggleButtonSelector, e => { + this.registerEventListener('on', $parentEl, 'click', this.toggleButtonSelector, (e) => { e.stopPropagation(); e.preventDefault(); this.showEmojiMenu($(e.currentTarget)); }); - this.registerEventListener('on', $('html'), 'click', e => { + this.registerEventListener('on', $('html'), 'click', (e) => { const $target = $(e.target); if (!$target.closest(`.${this.menuClass}`).length) { $('.js-awards-block.current').removeClass('current'); @@ -74,7 +74,7 @@ export class AwardsHandler { }); const emojiButtonSelector = `.js-awards-block .js-emoji-btn, .${this.menuClass} .js-emoji-btn`; - this.registerEventListener('on', $parentEl, 'click', emojiButtonSelector, e => { + this.registerEventListener('on', $parentEl, 'click', emojiButtonSelector, (e) => { e.preventDefault(); const $target = $(e.currentTarget); const $glEmojiElement = $target.find('gl-emoji'); @@ -98,10 +98,7 @@ export class AwardsHandler { showEmojiMenu($addBtn) { if ($addBtn.hasClass('js-note-emoji')) { - $addBtn - .closest('.note') - .find('.js-awards-block') - .addClass('current'); + $addBtn.closest('.note').find('.js-awards-block').addClass('current'); } else { $addBtn.closest('.js-awards-block').addClass('current'); } @@ -193,7 +190,7 @@ export class AwardsHandler { (promiseChain, categoryNameKey) => promiseChain.then( () => - new Promise(resolve => { + new Promise((resolve) => { const emojisInCategory = categoryMap[categoryNameKey]; const categoryMarkup = this.renderCategory( categoryLabelMap[categoryNameKey], @@ -216,7 +213,7 @@ export class AwardsHandler { menu.dispatchEvent(new CustomEvent('build-emoji-menu-finish')); } }) - .catch(err => { + .catch((err) => { emojiContentElement.insertAdjacentHTML( 'beforeend', '<p>We encountered an error while adding the remaining categories</p>', @@ -233,7 +230,7 @@ export class AwardsHandler { <ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}"> ${emojiList .map( - emojiName => ` + (emojiName) => ` <li class="emoji-menu-list-item"> <button class="emoji-menu-btn text-center js-emoji-btn" type="button"> ${this.emoji.glEmojiTag(emojiName, { @@ -466,7 +463,7 @@ export class AwardsHandler { const className = 'pulse animated once short'; $emoji.addClass(className); - this.registerEventListener('on', $emoji, animationEndEventString, e => { + this.registerEventListener('on', $emoji, animationEndEventString, (e) => { $(e.currentTarget).removeClass(className); }); } @@ -518,7 +515,7 @@ export class AwardsHandler { this.frequentlyUsedEmojis || (() => { const frequentlyUsedEmojis = uniq((Cookies.get('frequently_used_emojis') || '').split(',')); - this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(inputName => + this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter((inputName) => this.emoji.isEmojiNameValid(inputName), ); @@ -530,15 +527,13 @@ export class AwardsHandler { setupSearch() { const $search = $('.js-emoji-menu-search'); - this.registerEventListener('on', $search, 'input', e => { - const term = $(e.target) - .val() - .trim(); + this.registerEventListener('on', $search, 'input', (e) => { + const term = $(e.target).val().trim(); this.searchEmojis(term); }); const $menu = $(`.${this.menuClass}`); - this.registerEventListener('on', $menu, transitionEndEventString, e => { + this.registerEventListener('on', $menu, transitionEndEventString, (e) => { if (e.target === e.currentTarget) { // Clear the search this.searchEmojis(''); @@ -556,17 +551,11 @@ export class AwardsHandler { // Generate a search result block const h5 = $('<h5 class="emoji-search-title"/>').text('Search results'); const foundEmojis = this.findMatchingEmojiElements(term).show(); - const ul = $('<ul>') - .addClass('emoji-menu-list emoji-menu-search') - .append(foundEmojis); + const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis); $('.emoji-menu-content ul, .emoji-menu-content h5').hide(); - $('.emoji-menu-content') - .append(h5) - .append(ul); + $('.emoji-menu-content').append(h5).append(ul); } else { - $('.emoji-menu-content') - .children() - .show(); + $('.emoji-menu-content').children().show(); } } @@ -594,7 +583,7 @@ export class AwardsHandler { } hideMenuElement($emojiMenu) { - $emojiMenu.on(transitionEndEventString, e => { + $emojiMenu.on(transitionEndEventString, (e) => { if (e.currentTarget === e.target) { // eslint-disable-next-line @gitlab/no-global-event-off $emojiMenu.removeClass(IS_RENDERED).off(transitionEndEventString); @@ -605,7 +594,7 @@ export class AwardsHandler { } destroy() { - this.eventListeners.forEach(entry => { + this.eventListeners.forEach((entry) => { entry.element.off.call(entry.element, ...entry.args); }); $(`.${this.menuClass}`).remove(); diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue index 0a8479519f1..9e09f527a39 100644 --- a/app/assets/javascripts/badges/components/badge_form.vue +++ b/app/assets/javascripts/badges/components/badge_form.vue @@ -48,10 +48,10 @@ export default { }, helpText() { const placeholders = ['project_path', 'project_id', 'default_branch', 'commit_sha'] - .map(placeholder => `<code>%{${placeholder}}</code>`) + .map((placeholder) => `<code>%{${placeholder}}</code>`) .join(', '); return sprintf( - s__('Badges|The %{docsLinkStart}variables%{docsLinkEnd} GitLab supports: %{placeholders}'), + s__('Badges|Supported %{docsLinkStart}variables%{docsLinkEnd}: %{placeholders}'), { docsLinkEnd: '</a>', docsLinkStart: `<a href="${escape(this.docsUrl)}">`, @@ -105,13 +105,13 @@ export default { badgeImageUrlExample() { const exampleUrl = 'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/pipeline.svg'; - return sprintf(s__('Badges|e.g. %{exampleUrl}'), { + return sprintf(s__('Badges|Example: %{exampleUrl}'), { exampleUrl, }); }, badgeLinkUrlExample() { const exampleUrl = 'https://example.gitlab.com/%{project_path}'; - return sprintf(s__('Badges|e.g. %{exampleUrl}'), { + return sprintf(s__('Badges|Example: %{exampleUrl}'), { exampleUrl, }); }, @@ -134,10 +134,10 @@ export default { if (this.isEditing) { return this.saveBadge() .then(() => { - createFlash(s__('Badges|The badge was saved.'), 'notice'); + createFlash(s__('Badges|Badge saved.'), 'notice'); this.wasValidated = false; }) - .catch(error => { + .catch((error) => { createFlash( s__('Badges|Saving the badge failed, please check the entered URLs and try again.'), ); @@ -147,10 +147,10 @@ export default { return this.addBadge() .then(() => { - createFlash(s__('Badges|A new badge was added.'), 'notice'); + createFlash(s__('Badges|New badge added.'), 'notice'); this.wasValidated = false; }) - .catch(error => { + .catch((error) => { createFlash( s__('Badges|Adding the badge failed, please check the entered URLs and try again.'), ); @@ -183,7 +183,7 @@ export default { required @input="debouncedPreview" /> - <div class="invalid-feedback">{{ s__('Badges|Please fill in a valid URL') }}</div> + <div class="invalid-feedback">{{ s__('Badges|Enter a valid URL') }}</div> <span class="form-text text-muted">{{ badgeLinkUrlExample }}</span> </div> @@ -198,7 +198,7 @@ export default { required @input="debouncedPreview" /> - <div class="invalid-feedback">{{ s__('Badges|Please fill in a valid URL') }}</div> + <div class="invalid-feedback">{{ s__('Badges|Enter a valid URL') }}</div> <span class="form-text text-muted">{{ badgeImageUrlExample }}</span> </div> diff --git a/app/assets/javascripts/badges/components/badge_settings.vue b/app/assets/javascripts/badges/components/badge_settings.vue index 19781783100..73c63a72b1c 100644 --- a/app/assets/javascripts/badges/components/badge_settings.vue +++ b/app/assets/javascripts/badges/components/badge_settings.vue @@ -42,7 +42,7 @@ export default { .then(() => { createFlash(s__('Badges|The badge was deleted.'), 'notice'); }) - .catch(error => { + .catch((error) => { createFlash(s__('Badges|Deleting the badge failed, please try again.')); throw error; }); diff --git a/app/assets/javascripts/badges/store/actions.js b/app/assets/javascripts/badges/store/actions.js index 806c2423e7e..3377f6c0996 100644 --- a/app/assets/javascripts/badges/store/actions.js +++ b/app/assets/javascripts/badges/store/actions.js @@ -2,7 +2,7 @@ import axios from '~/lib/utils/axios_utils'; import types from './mutation_types'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -export const transformBackendBadge = badge => ({ +export const transformBackendBadge = (badge) => ({ ...convertObjectPropsToCamelCase(badge, true), isDeleting: false, }); @@ -27,11 +27,11 @@ export default { image_url: newBadge.imageUrl, link_url: newBadge.linkUrl, }) - .catch(error => { + .catch((error) => { dispatch('receiveNewBadgeError'); throw error; }) - .then(res => { + .then((res) => { dispatch('receiveNewBadge', transformBackendBadge(res.data)); }); }, @@ -50,7 +50,7 @@ export default { const endpoint = `${state.apiEndpointUrl}/${badgeId}`; return axios .delete(endpoint) - .catch(error => { + .catch((error) => { dispatch('receiveDeleteBadgeError', badgeId); throw error; }) @@ -78,11 +78,11 @@ export default { const endpoint = state.apiEndpointUrl; return axios .get(endpoint) - .catch(error => { + .catch((error) => { dispatch('receiveLoadBadgesError'); throw error; }) - .then(res => { + .then((res) => { dispatch('receiveLoadBadges', res.data.map(transformBackendBadge)); }); }, @@ -113,11 +113,11 @@ export default { const renderEndpoint = `${state.apiEndpointUrl}/render?${parameters}`; return axios .get(renderEndpoint) - .catch(error => { + .catch((error) => { dispatch('receiveRenderedBadgeError'); throw error; }) - .then(res => { + .then((res) => { dispatch('receiveRenderedBadge', transformBackendBadge(res.data)); }); }, @@ -142,11 +142,11 @@ export default { image_url: badge.imageUrl, link_url: badge.linkUrl, }) - .catch(error => { + .catch((error) => { dispatch('receiveUpdatedBadgeError'); throw error; }) - .then(res => { + .then((res) => { dispatch('receiveUpdatedBadge', transformBackendBadge(res.data)); }); }, diff --git a/app/assets/javascripts/badges/store/mutations.js b/app/assets/javascripts/badges/store/mutations.js index bd84e68c00f..3f4689aeb17 100644 --- a/app/assets/javascripts/badges/store/mutations.js +++ b/app/assets/javascripts/badges/store/mutations.js @@ -1,7 +1,7 @@ import types from './mutation_types'; import { PROJECT_BADGE } from '../constants'; -const reorderBadges = badges => +const reorderBadges = (badges) => badges.sort((a, b) => { if (a.kind !== b.kind) { return a.kind === PROJECT_BADGE ? 1 : -1; @@ -31,7 +31,7 @@ export default { }, [types.RECEIVE_UPDATED_BADGE](state, updatedBadge) { - const badges = state.badges.map(badge => { + const badges = state.badges.map((badge) => { if (badge.id === updatedBadge.id) { return updatedBadge; } @@ -77,13 +77,13 @@ export default { }, [types.RECEIVE_DELETE_BADGE](state, badgeId) { - const badges = state.badges.filter(badge => badge.id !== badgeId); + const badges = state.badges.filter((badge) => badge.id !== badgeId); Object.assign(state, { badges, }); }, [types.RECEIVE_DELETE_BADGE_ERROR](state, badgeId) { - const badges = state.badges.map(badge => { + const badges = state.badges.map((badge) => { if (badge.id === badgeId) { return { ...badge, @@ -98,7 +98,7 @@ export default { }); }, [types.REQUEST_DELETE_BADGE](state, badgeId) { - const badges = state.badges.map(badge => { + const badges = state.badges.map((badge) => { if (badge.id === badgeId) { return { ...badge, diff --git a/app/assets/javascripts/batch_comments/components/preview_item.vue b/app/assets/javascripts/batch_comments/components/preview_item.vue index dca6d90fbcb..3e93168f0e2 100644 --- a/app/assets/javascripts/batch_comments/components/preview_item.vue +++ b/app/assets/javascripts/batch_comments/components/preview_item.vue @@ -47,7 +47,7 @@ export default { } return sprintf(__("%{authorsName}'s thread"), { - authorsName: this.discussion.notes.find(note => !note.system).author.name, + authorsName: this.discussion.notes.find((note) => !note.system).author.name, }); }, linePosition() { @@ -98,9 +98,7 @@ export default { {{ titleText }} </span> <template v-if="showLinePosition"> - <template v-if="!glFeatures.multilineComments" - >:{{ linePosition }}</template - > + <template v-if="!glFeatures.multilineComments">:{{ linePosition }}</template> <template v-else-if="startLineNumber === endLineNumber"> :<span :class="getLineClasses(startLineNumber)">{{ startLineNumber }}</span> </template> diff --git a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js index ebd821125fb..a29409c52ae 100644 --- a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js +++ b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js @@ -11,8 +11,8 @@ export const saveDraft = ({ dispatch }, draft) => export const addDraftToDiscussion = ({ commit }, { endpoint, data }) => service .addDraftToDiscussion(endpoint, data) - .then(res => res.data) - .then(res => { + .then((res) => res.data) + .then((res) => { commit(types.ADD_NEW_DRAFT, res); return res; }) @@ -23,8 +23,8 @@ export const addDraftToDiscussion = ({ commit }, { endpoint, data }) => export const createNewDraft = ({ commit }, { endpoint, data }) => service .createNewDraft(endpoint, data) - .then(res => res.data) - .then(res => { + .then((res) => res.data) + .then((res) => { commit(types.ADD_NEW_DRAFT, res); return res; }) @@ -43,8 +43,8 @@ export const deleteDraft = ({ commit, getters }, draft) => export const fetchDrafts = ({ commit, getters }) => service .fetchDrafts(getters.getNotesData.draftsPath) - .then(res => res.data) - .then(data => commit(types.SET_BATCH_COMMENTS_DRAFTS, data)) + .then((res) => res.data) + .then((data) => commit(types.SET_BATCH_COMMENTS_DRAFTS, data)) .catch(() => flash(__('An error occurred while fetching pending comments'))); export const publishSingleDraft = ({ commit, dispatch, getters }, draftId) => { @@ -86,8 +86,8 @@ export const updateDraft = ( resolveDiscussion, position: JSON.stringify(position), }) - .then(res => res.data) - .then(data => commit(types.RECEIVE_DRAFT_UPDATE_SUCCESS, data)) + .then((res) => res.data) + .then((data) => commit(types.RECEIVE_DRAFT_UPDATE_SUCCESS, data)) .then(callback) .catch(() => flash(__('An error occurred while updating the comment'))); @@ -116,8 +116,8 @@ export const scrollToDraft = ({ dispatch, rootGetters }, draft) => { export const expandAllDiscussions = ({ dispatch, state }) => state.drafts - .filter(draft => draft.discussion_id) - .forEach(draft => { + .filter((draft) => draft.discussion_id) + .forEach((draft) => { dispatch('expandDiscussion', { discussionId: draft.discussion_id }, { root: true }); }); diff --git a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/getters.js b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/getters.js index 22ae6c2e970..df5214ea7ab 100644 --- a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/getters.js +++ b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/getters.js @@ -1,12 +1,12 @@ import { parallelLineKey, showDraftOnSide } from '../../../utils'; -export const draftsCount = state => state.drafts.length; +export const draftsCount = (state) => state.drafts.length; export const getNotesData = (state, getters, rootState, rootGetters) => rootGetters.getNotesData; -export const hasDrafts = state => state.drafts.length > 0; +export const hasDrafts = (state) => state.drafts.length > 0; -export const draftsPerDiscussionId = state => +export const draftsPerDiscussionId = (state) => state.drafts.reduce((acc, draft) => { if (draft.discussion_id) { acc[draft.discussion_id] = draft; @@ -15,7 +15,7 @@ export const draftsPerDiscussionId = state => return acc; }, {}); -export const draftsPerFileHashAndLine = state => +export const draftsPerFileHashAndLine = (state) => state.drafts.reduce((acc, draft) => { if (draft.file_hash) { if (!acc[draft.file_hash]) { @@ -55,10 +55,10 @@ export const hasParallelDraftRight = (state, getters) => (diffFileSha, line) => return draftsForFile ? Boolean(draftsForFile[rkey]) : false; }; -export const shouldRenderDraftRowInDiscussion = (state, getters) => discussionId => +export const shouldRenderDraftRowInDiscussion = (state, getters) => (discussionId) => typeof getters.draftsPerDiscussionId[discussionId] !== 'undefined'; -export const draftForDiscussion = (state, getters) => discussionId => +export const draftForDiscussion = (state, getters) => (discussionId) => getters.draftsPerDiscussionId[discussionId] || {}; export const draftForLine = (state, getters) => (diffFileSha, line, side = null) => { @@ -75,10 +75,10 @@ export const draftForLine = (state, getters) => (diffFileSha, line, side = null) return {}; }; -export const draftsForFile = state => diffFileSha => - state.drafts.filter(draft => draft.file_hash === diffFileSha); +export const draftsForFile = (state) => (diffFileSha) => + state.drafts.filter((draft) => draft.file_hash === diffFileSha); -export const isPublishingDraft = state => draftId => +export const isPublishingDraft = (state) => (draftId) => state.currentlyPublishingDrafts.indexOf(draftId) !== -1; -export const sortedDrafts = state => [...state.drafts].sort((a, b) => a.id > b.id); +export const sortedDrafts = (state) => [...state.drafts].sort((a, b) => a.id > b.id); diff --git a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/mutations.js b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/mutations.js index 731f4b6d12a..dabfe864575 100644 --- a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/mutations.js +++ b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/mutations.js @@ -1,6 +1,6 @@ import * as types from './mutation_types'; -const processDraft = draft => ({ +const processDraft = (draft) => ({ ...draft, isDraft: true, }); @@ -11,7 +11,7 @@ export default { }, [types.DELETE_DRAFT](state, draftId) { - state.drafts = state.drafts.filter(draft => draft.id !== draftId); + state.drafts = state.drafts.filter((draft) => draft.id !== draftId); }, [types.SET_BATCH_COMMENTS_DRAFTS](state, drafts) { @@ -23,13 +23,13 @@ export default { }, [types.RECEIVE_PUBLISH_DRAFT_SUCCESS](state, draftId) { state.currentlyPublishingDrafts = state.currentlyPublishingDrafts.filter( - publishingDraftId => publishingDraftId !== draftId, + (publishingDraftId) => publishingDraftId !== draftId, ); - state.drafts = state.drafts.filter(d => d.id !== draftId); + state.drafts = state.drafts.filter((d) => d.id !== draftId); }, [types.RECEIVE_PUBLISH_DRAFT_ERROR](state, draftId) { state.currentlyPublishingDrafts = state.currentlyPublishingDrafts.filter( - publishingDraftId => publishingDraftId !== draftId, + (publishingDraftId) => publishingDraftId !== draftId, ); }, @@ -44,14 +44,14 @@ export default { state.isPublishing = false; }, [types.RECEIVE_DRAFT_UPDATE_SUCCESS](state, data) { - const index = state.drafts.findIndex(draft => draft.id === data.id); + const index = state.drafts.findIndex((draft) => draft.id === data.id); if (index >= 0) { state.drafts.splice(index, 1, processDraft(data)); } }, [types.TOGGLE_RESOLVE_DISCUSSION](state, draftId) { - state.drafts = state.drafts.map(draft => { + state.drafts = state.drafts.map((draft) => { if (draft.id === draftId) { return { ...draft, diff --git a/app/assets/javascripts/batch_comments/utils.js b/app/assets/javascripts/batch_comments/utils.js index cf4f7af0ebb..387e7e4a610 100644 --- a/app/assets/javascripts/batch_comments/utils.js +++ b/app/assets/javascripts/batch_comments/utils.js @@ -1,11 +1,11 @@ import { getFormData } from '~/diffs/store/utils'; -export const getDraftReplyFormData = data => ({ +export const getDraftReplyFormData = (data) => ({ endpoint: data.notesData.draftsPath, data, }); -export const getDraftFormData = params => ({ +export const getDraftFormData = (params) => ({ endpoint: params.notesData.draftsPath, data: getFormData(params), }); diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js index 3e9d77cdf6b..a5404539c17 100644 --- a/app/assets/javascripts/behaviors/autosize.js +++ b/app/assets/javascripts/behaviors/autosize.js @@ -8,6 +8,6 @@ document.addEventListener('DOMContentLoaded', () => { Autosize(autosizeEls); Autosize.update(autosizeEls); - autosizeEls.forEach(el => el.classList.add('js-autosize-initialized')); + autosizeEls.forEach((el) => el.classList.add('js-autosize-initialized')); }); }); diff --git a/app/assets/javascripts/behaviors/bind_in_out.js b/app/assets/javascripts/behaviors/bind_in_out.js index 886f127b06b..2c5a3994ade 100644 --- a/app/assets/javascripts/behaviors/bind_in_out.js +++ b/app/assets/javascripts/behaviors/bind_in_out.js @@ -30,7 +30,7 @@ class BindInOut { static initAll() { const ins = document.querySelectorAll('*[data-bind-in]'); - return [].map.call(ins, anIn => BindInOut.init(anIn)); + return [].map.call(ins, (anIn) => BindInOut.init(anIn)); } static init(anIn, anOut) { diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js index e822072d669..1176fa6628d 100644 --- a/app/assets/javascripts/behaviors/copy_to_clipboard.js +++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js @@ -58,7 +58,7 @@ export default function initCopyToClipboard() { * the last minute to deconstruct this JSON hash and set the `text/plain` and `text/x-gfm` copy * data types to the intended values. */ - $(document).on('copy', 'body > textarea[readonly]', e => { + $(document).on('copy', 'body > textarea[readonly]', (e) => { const { clipboardData } = e.originalEvent; if (!clipboardData) return; diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js index 16373b523b2..1fa37999d62 100644 --- a/app/assets/javascripts/behaviors/gl_emoji.js +++ b/app/assets/javascripts/behaviors/gl_emoji.js @@ -32,7 +32,7 @@ class GlEmoji extends HTMLElement { const isEmojiUnicode = this.childNodes && - Array.prototype.every.call(this.childNodes, childNode => childNode.nodeType === 3); + Array.prototype.every.call(this.childNodes, (childNode) => childNode.nodeType === 3); if ( emojiUnicode && diff --git a/app/assets/javascripts/behaviors/load_startup_css.js b/app/assets/javascripts/behaviors/load_startup_css.js index 1d7bf716475..dbe9ff8b6e7 100644 --- a/app/assets/javascripts/behaviors/load_startup_css.js +++ b/app/assets/javascripts/behaviors/load_startup_css.js @@ -8,7 +8,7 @@ export const loadStartupCSS = () => { () => { document .querySelectorAll('link[media=print]') - .forEach(x => x.dispatchEvent(new Event('load'))); + .forEach((x) => x.dispatchEvent(new Event('load'))); }, { once: true }, ); diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js index ce5b63df19c..9a8af79210e 100644 --- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js @@ -10,10 +10,10 @@ export class CopyAsGFM { const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent); if (isIOS) return; - $(document).on('copy', '.md', e => { + $(document).on('copy', '.md', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); }); - $(document).on('copy', 'pre.code.highlight, table.code td.line_content', e => { + $(document).on('copy', 'pre.code.highlight, table.code td.line_content', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); }); $(document).on('paste', '.js-gfm-input', CopyAsGFM.pasteGFM); @@ -42,7 +42,7 @@ export class CopyAsGFM { clipboardData.setData('text/x-gfm-html', html); CopyAsGFM.nodeToGFM(el) - .then(res => { + .then((res) => { clipboardData.setData('text/x-gfm', res); }) .catch(() => { @@ -71,7 +71,7 @@ export class CopyAsGFM { const div = document.createElement('div'); div.innerHTML = gfmHtml; CopyAsGFM.nodeToGFM(div) - .then(transformedGfm => { + .then((transformedGfm) => { CopyAsGFM.insertPastedText(e.target, text, transformedGfm); }) .catch(() => {}); @@ -79,7 +79,7 @@ export class CopyAsGFM { } static insertPastedText(target, text, gfm) { - insertText(target, textBefore => { + insertText(target, (textBefore) => { // If the text before the cursor contains an odd number of backticks, // we are either inside an inline code span that starts with 1 backtick // or a code block that starts with 3 backticks. @@ -125,7 +125,7 @@ export class CopyAsGFM { let lineSelector = '.line'; if (target) { - const lineClass = ['left-side', 'right-side'].filter(name => + const lineClass = ['left-side', 'right-side'].filter((name) => target.classList.contains(name), )[0]; if (lineClass) { diff --git a/app/assets/javascripts/behaviors/markdown/highlight_current_user.js b/app/assets/javascripts/behaviors/markdown/highlight_current_user.js index 6208b3f0032..0fac278573e 100644 --- a/app/assets/javascripts/behaviors/markdown/highlight_current_user.js +++ b/app/assets/javascripts/behaviors/markdown/highlight_current_user.js @@ -9,7 +9,7 @@ export default function highlightCurrentUser(elements) { return; } - elements.forEach(element => { + elements.forEach((element) => { if (parseInt(element.dataset.user, 10) === currentUserId) { element.classList.add('current-user'); } diff --git a/app/assets/javascripts/behaviors/markdown/marks/inline_diff.js b/app/assets/javascripts/behaviors/markdown/marks/inline_diff.js index ce425e80cd3..7f1506cd5d9 100644 --- a/app/assets/javascripts/behaviors/markdown/marks/inline_diff.js +++ b/app/assets/javascripts/behaviors/markdown/marks/inline_diff.js @@ -19,7 +19,7 @@ export default class InlineDiff extends Mark { { tag: 'span.idiff.addition', attrs: { addition: true } }, { tag: 'span.idiff.deletion', attrs: { addition: false } }, ], - toDOM: node => [ + toDOM: (node) => [ 'span', { class: `idiff left right ${node.attrs.addition ? 'addition' : 'deletion'}` }, 0, diff --git a/app/assets/javascripts/behaviors/markdown/marks/inline_html.js b/app/assets/javascripts/behaviors/markdown/marks/inline_html.js index f8465111959..556e6f7df1c 100644 --- a/app/assets/javascripts/behaviors/markdown/marks/inline_html.js +++ b/app/assets/javascripts/behaviors/markdown/marks/inline_html.js @@ -19,14 +19,14 @@ export default class InlineHTML extends Mark { parseDOM: [ { tag: 'sup, sub, kbd, q, samp, var', - getAttrs: el => ({ tag: el.nodeName.toLowerCase() }), + getAttrs: (el) => ({ tag: el.nodeName.toLowerCase() }), }, { tag: 'abbr', - getAttrs: el => ({ tag: 'abbr', title: el.getAttribute('title') }), + getAttrs: (el) => ({ tag: 'abbr', title: el.getAttribute('title') }), }, ], - toDOM: node => [node.attrs.tag, { title: node.attrs.title }, 0], + toDOM: (node) => [node.attrs.tag, { title: node.attrs.title }, 0], }; } diff --git a/app/assets/javascripts/behaviors/markdown/nodes/code_block.js b/app/assets/javascripts/behaviors/markdown/nodes/code_block.js index 1e0c05eff08..cd90d67c60d 100644 --- a/app/assets/javascripts/behaviors/markdown/nodes/code_block.js +++ b/app/assets/javascripts/behaviors/markdown/nodes/code_block.js @@ -25,7 +25,7 @@ export default class CodeBlock extends BaseCodeBlock { { tag: 'pre.code.highlight', preserveWhitespace: 'full', - getAttrs: el => { + getAttrs: (el) => { const lang = el.getAttribute('lang'); if (!lang || lang === '') return {}; @@ -62,13 +62,13 @@ export default class CodeBlock extends BaseCodeBlock { tag: '.md-suggestion-diff', preserveWhitespace: 'full', getContent: (el, schema) => - [...el.querySelectorAll('.line_content.new span')].map(span => + [...el.querySelectorAll('.line_content.new span')].map((span) => schema.text(span.innerText), ), attrs: { lang: 'suggestion' }, }, ], - toDOM: node => ['pre', { class: 'code highlight', lang: node.attrs.lang }, ['code', 0]], + toDOM: (node) => ['pre', { class: 'code highlight', lang: node.attrs.lang }, ['code', 0]], }; } diff --git a/app/assets/javascripts/behaviors/markdown/nodes/emoji.js b/app/assets/javascripts/behaviors/markdown/nodes/emoji.js index a7cc3e828f5..367a06ad3c1 100644 --- a/app/assets/javascripts/behaviors/markdown/nodes/emoji.js +++ b/app/assets/javascripts/behaviors/markdown/nodes/emoji.js @@ -20,14 +20,14 @@ export default class Emoji extends Node { parseDOM: [ { tag: 'gl-emoji', - getAttrs: el => ({ + getAttrs: (el) => ({ name: el.dataset.name, title: el.getAttribute('title'), moji: el.textContent, }), }, ], - toDOM: node => [ + toDOM: (node) => [ 'gl-emoji', { 'data-name': node.attrs.name, title: node.attrs.title }, node.attrs.moji, diff --git a/app/assets/javascripts/behaviors/markdown/nodes/image.js b/app/assets/javascripts/behaviors/markdown/nodes/image.js index b1983eebe15..76746528e72 100644 --- a/app/assets/javascripts/behaviors/markdown/nodes/image.js +++ b/app/assets/javascripts/behaviors/markdown/nodes/image.js @@ -30,7 +30,7 @@ export default class Image extends BaseImage { // Matches HTML generated by Banzai::Filter::ImageLazyLoadFilter { tag: 'img[src]', - getAttrs: el => { + getAttrs: (el) => { const imageSrc = el.src; const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : el.dataset.src || ''; @@ -43,7 +43,7 @@ export default class Image extends BaseImage { }, }, ], - toDOM: node => ['img', node.attrs], + toDOM: (node) => ['img', node.attrs], }; } diff --git a/app/assets/javascripts/behaviors/markdown/nodes/playable.js b/app/assets/javascripts/behaviors/markdown/nodes/playable.js index 9a2c9c3c9b0..9cbd95a7bd8 100644 --- a/app/assets/javascripts/behaviors/markdown/nodes/playable.js +++ b/app/assets/javascripts/behaviors/markdown/nodes/playable.js @@ -42,11 +42,11 @@ export default class Playable extends Node { }, { tag: `${this.mediaType}[src]`, - getAttrs: el => ({ src: el.src, alt: el.dataset.title }), + getAttrs: (el) => ({ src: el.src, alt: el.dataset.title }), }, ]; - const toDOM = node => [ + const toDOM = (node) => [ this.mediaType, { src: node.attrs.src, diff --git a/app/assets/javascripts/behaviors/markdown/nodes/reference.js b/app/assets/javascripts/behaviors/markdown/nodes/reference.js index aa724798da6..dd82ea58ea5 100644 --- a/app/assets/javascripts/behaviors/markdown/nodes/reference.js +++ b/app/assets/javascripts/behaviors/markdown/nodes/reference.js @@ -25,7 +25,7 @@ export default class Reference extends Node { { tag: 'a.gfm:not([data-link=true])', priority: HIGHER_PARSE_RULE_PRIORITY, - getAttrs: el => ({ + getAttrs: (el) => ({ className: el.className, referenceType: el.dataset.referenceType, originalText: el.dataset.original, @@ -34,7 +34,7 @@ export default class Reference extends Node { }), }, ], - toDOM: node => [ + toDOM: (node) => [ 'a', { class: node.attrs.className, diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_cell.js b/app/assets/javascripts/behaviors/markdown/nodes/table_cell.js index c63bfe10e39..ebb66cd4da5 100644 --- a/app/assets/javascripts/behaviors/markdown/nodes/table_cell.js +++ b/app/assets/javascripts/behaviors/markdown/nodes/table_cell.js @@ -19,13 +19,13 @@ export default class TableCell extends Node { parseDOM: [ { tag: 'td, th', - getAttrs: el => ({ + getAttrs: (el) => ({ header: el.tagName === 'TH', align: el.getAttribute('align') || el.style.textAlign, }), }, ], - toDOM: node => [node.attrs.header ? 'th' : 'td', { align: node.attrs.align }, 0], + toDOM: (node) => [node.attrs.header ? 'th' : 'td', { align: node.attrs.align }, 0], }; } diff --git a/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js b/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js index 7bb56b4c406..56c2b17286d 100644 --- a/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js +++ b/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js @@ -23,7 +23,7 @@ export default class TaskListItem extends Node { { priority: HIGHER_PARSE_RULE_PRIORITY, tag: 'li.task-list-item', - getAttrs: el => { + getAttrs: (el) => { const checkbox = el.querySelector('input[type=checkbox].task-list-item-checkbox'); return { done: checkbox && checkbox.checked }; }, diff --git a/app/assets/javascripts/behaviors/markdown/paste_markdown_table.js b/app/assets/javascripts/behaviors/markdown/paste_markdown_table.js index 278dd857ab8..46582867d05 100644 --- a/app/assets/javascripts/behaviors/markdown/paste_markdown_table.js +++ b/app/assets/javascripts/behaviors/markdown/paste_markdown_table.js @@ -1,4 +1,5 @@ -const maxColumnWidth = (rows, columnIndex) => Math.max(...rows.map(row => row[columnIndex].length)); +const maxColumnWidth = (rows, columnIndex) => + Math.max(...rows.map((row) => row[columnIndex].length)); export default class PasteMarkdownTable { constructor(clipboardData) { @@ -16,7 +17,7 @@ export default class PasteMarkdownTable { this.calculateColumnWidths(); const markdownRows = this.rows.map( - row => + (row) => // | Name | Title | Email Address | // |--------------|-------|----------------| // | Jane Atler | CEO | jane@acme.com | @@ -66,7 +67,7 @@ export default class PasteMarkdownTable { return false; } - this.rows = splitRows.map(row => row.split('\t')); + this.rows = splitRows.map((row) => row.split('\t')); this.normalizeRows(); // Check that the max number of columns in the HTML matches the number of @@ -81,10 +82,10 @@ export default class PasteMarkdownTable { // Ensure each row has the same number of columns normalizeRows() { - const rowLengths = this.rows.map(row => row.length); + const rowLengths = this.rows.map((row) => row.length); const maxLength = Math.max(...rowLengths); - this.rows.forEach(row => { + this.rows.forEach((row) => { while (row.length < maxLength) { row.push(''); } @@ -101,7 +102,7 @@ export default class PasteMarkdownTable { const textColumnCount = this.rows[0].length; let htmlColumnCount = 0; - this.doc.querySelectorAll('table tr').forEach(row => { + this.doc.querySelectorAll('table tr').forEach((row) => { htmlColumnCount = Math.max(row.cells.length, htmlColumnCount); }); diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js index 30783562da9..f34fec4d449 100644 --- a/app/assets/javascripts/behaviors/markdown/render_math.js +++ b/app/assets/javascripts/behaviors/markdown/render_math.js @@ -35,7 +35,7 @@ const RENDER_FLASH_MSG = sprintf( // Wait for the browser to reflow the layout. Reflowing SVG takes time. // This has to wrap the inner function, otherwise IE/Edge throw "invalid calling object". -const waitForReflow = fn => { +const waitForReflow = (fn) => { window.requestAnimationFrame(fn); }; @@ -122,7 +122,7 @@ class SafeMathRenderer { render() { // Replace math blocks with a placeholder so they aren't rendered twice - this.elements.forEach(el => { + this.elements.forEach((el) => { const placeholder = document.createElement('span'); placeholder.style.display = 'none'; placeholder.setAttribute('data-math-style', el.getAttribute('data-math-style')); diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js index 602f156dbf0..479782a1f1f 100644 --- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js +++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js @@ -29,7 +29,7 @@ let mermaidModule = {}; function importMermaidModule() { return import(/* webpackChunkName: 'mermaid' */ 'mermaid') - .then(mermaid => { + .then((mermaid) => { let theme = 'neutral'; const ideDarkThemes = ['dark', 'solarized-dark', 'monokai']; @@ -59,7 +59,7 @@ function importMermaidModule() { return mermaid; }) - .catch(err => { + .catch((err) => { flash(sprintf(__("Can't load mermaid module: %{err}"), { err })); // eslint-disable-next-line no-console console.error(err); @@ -77,7 +77,7 @@ function fixElementSource(el) { } function renderMermaidEl(el) { - mermaidModule.init(undefined, el, id => { + mermaidModule.init(undefined, el, (id) => { const source = el.textContent; const svg = document.getElementById(id); @@ -170,7 +170,7 @@ function renderMermaids($els) { elsProcessingMap.set(el, requestId); }); }) - .catch(err => { + .catch((err) => { flash(sprintf(__('Encountered an error while rendering: %{err}'), { err })); // eslint-disable-next-line no-console console.error(err); diff --git a/app/assets/javascripts/behaviors/markdown/render_metrics.js b/app/assets/javascripts/behaviors/markdown/render_metrics.js index 37cbce46b6f..e7a2a6ce47c 100644 --- a/app/assets/javascripts/behaviors/markdown/render_metrics.js +++ b/app/assets/javascripts/behaviors/markdown/render_metrics.js @@ -9,7 +9,7 @@ export default function renderMetrics(elements) { const wrapperList = []; - elements.forEach(element => { + elements.forEach((element) => { let wrapper; const { previousElementSibling } = element; const isFirstElementInGroup = !previousElementSibling?.urls; @@ -33,7 +33,7 @@ export default function renderMetrics(elements) { ).then(({ default: EmbedGroup }) => { const EmbedGroupComponent = Vue.extend(EmbedGroup); - wrapperList.forEach(wrapper => { + wrapperList.forEach((wrapper) => { // eslint-disable-next-line no-new new EmbedGroupComponent({ el: wrapper, diff --git a/app/assets/javascripts/behaviors/markdown/schema.js b/app/assets/javascripts/behaviors/markdown/schema.js index 163182ab778..8bea24584cc 100644 --- a/app/assets/javascripts/behaviors/markdown/schema.js +++ b/app/assets/javascripts/behaviors/markdown/schema.js @@ -2,7 +2,7 @@ import { Schema } from 'prosemirror-model'; import editorExtensions from './editor_extensions'; const nodes = editorExtensions - .filter(extension => extension.type === 'node') + .filter((extension) => extension.type === 'node') .reduce( (ns, { name, schema }) => ({ ...ns, @@ -12,7 +12,7 @@ const nodes = editorExtensions ); const marks = editorExtensions - .filter(extension => extension.type === 'mark') + .filter((extension) => extension.type === 'mark') .reduce( (ms, { name, schema }) => ({ ...ms, diff --git a/app/assets/javascripts/behaviors/markdown/serializer.js b/app/assets/javascripts/behaviors/markdown/serializer.js index 70dbd8bd206..b4adf1a413f 100644 --- a/app/assets/javascripts/behaviors/markdown/serializer.js +++ b/app/assets/javascripts/behaviors/markdown/serializer.js @@ -2,7 +2,7 @@ import { MarkdownSerializer } from 'prosemirror-markdown'; import editorExtensions from './editor_extensions'; const nodes = editorExtensions - .filter(extension => extension.type === 'node') + .filter((extension) => extension.type === 'node') .reduce( (ns, { name, toMarkdown }) => ({ ...ns, @@ -12,7 +12,7 @@ const nodes = editorExtensions ); const marks = editorExtensions - .filter(extension => extension.type === 'mark') + .filter((extension) => extension.type === 'mark') .reduce( (ms, { name, toMarkdown }) => ({ ...ms, diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js index 84bf22586a9..def1c567cd5 100644 --- a/app/assets/javascripts/behaviors/preview_markdown.js +++ b/app/assets/javascripts/behaviors/preview_markdown.js @@ -23,7 +23,7 @@ MarkdownPreview.prototype.emptyMessage = __('Nothing to preview.'); MarkdownPreview.prototype.ajaxCache = {}; -MarkdownPreview.prototype.showPreview = function($form) { +MarkdownPreview.prototype.showPreview = function ($form) { const preview = $form.find('.js-md-preview'); const url = preview.data('url'); if (preview.hasClass('md-preview-loading')) { @@ -41,7 +41,7 @@ MarkdownPreview.prototype.showPreview = function($form) { this.hideReferencedUsers($form); } else { preview.addClass('md-preview-loading').text(__('Loading...')); - this.fetchMarkdownPreview(mdText, url, response => { + this.fetchMarkdownPreview(mdText, url, (response) => { let body; if (response.body.length > 0) { ({ body } = response); @@ -60,7 +60,7 @@ MarkdownPreview.prototype.showPreview = function($form) { } }; -MarkdownPreview.prototype.fetchMarkdownPreview = function(text, url, success) { +MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) { if (!url) { return; } @@ -82,11 +82,11 @@ MarkdownPreview.prototype.fetchMarkdownPreview = function(text, url, success) { .catch(() => flash(__('An error occurred while fetching markdown preview'))); }; -MarkdownPreview.prototype.hideReferencedUsers = function($form) { +MarkdownPreview.prototype.hideReferencedUsers = function ($form) { $form.find('.referenced-users').hide(); }; -MarkdownPreview.prototype.renderReferencedUsers = function(users, $form) { +MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) { const referencedUsers = $form.find('.referenced-users'); if (referencedUsers.length) { if (users.length >= this.referenceThreshold) { @@ -98,11 +98,11 @@ MarkdownPreview.prototype.renderReferencedUsers = function(users, $form) { } }; -MarkdownPreview.prototype.hideReferencedCommands = function($form) { +MarkdownPreview.prototype.hideReferencedCommands = function ($form) { $form.find('.referenced-commands').hide(); }; -MarkdownPreview.prototype.renderReferencedCommands = function(commands, $form) { +MarkdownPreview.prototype.renderReferencedCommands = function (commands, $form) { const referencedCommands = $form.find('.referenced-commands'); if (commands.length > 0) { referencedCommands.html(commands); @@ -120,7 +120,7 @@ const writeButtonSelector = '.js-md-write-button'; lastTextareaPreviewed = null; const markdownToolbar = $('.md-header-toolbar'); -$.fn.setupMarkdownPreview = function() { +$.fn.setupMarkdownPreview = function () { const $form = $(this); $form.find('textarea.markdown-area').on('input', () => { markdownPreview.hideReferencedUsers($form); @@ -136,14 +136,8 @@ $(document).on('markdown-preview:show', (e, $form) => { lastTextareaHeight = lastTextareaPreviewed.height(); // toggle tabs - $form - .find(writeButtonSelector) - .parent() - .removeClass('active'); - $form - .find(previewButtonSelector) - .parent() - .addClass('active'); + $form.find(writeButtonSelector).parent().removeClass('active'); + $form.find(previewButtonSelector).parent().addClass('active'); // toggle content $form.find('.md-write-holder').hide(); @@ -163,14 +157,8 @@ $(document).on('markdown-preview:hide', (e, $form) => { } // toggle tabs - $form - .find(writeButtonSelector) - .parent() - .addClass('active'); - $form - .find(previewButtonSelector) - .parent() - .removeClass('active'); + $form.find(writeButtonSelector).parent().addClass('active'); + $form.find(previewButtonSelector).parent().removeClass('active'); // toggle content $form.find('.md-write-holder').show(); @@ -194,13 +182,13 @@ $(document).on('markdown-preview:toggle', (e, keyboardEvent) => { } }); -$(document).on('click', previewButtonSelector, function(e) { +$(document).on('click', previewButtonSelector, function (e) { e.preventDefault(); const $form = $(this).closest('form'); $(document).triggerHandler('markdown-preview:show', [$form]); }); -$(document).on('click', writeButtonSelector, function(e) { +$(document).on('click', writeButtonSelector, function (e) { e.preventDefault(); const $form = $(this).closest('form'); $(document).triggerHandler('markdown-preview:hide', [$form]); diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index 907cfc06e28..68e831252d6 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -30,7 +30,7 @@ function keyCodeIs(e, keyCode) { return e.keyCode === keyCode; } -$(document).on('keydown.quick_submit', '.js-quick-submit', e => { +$(document).on('keydown.quick_submit', '.js-quick-submit', (e) => { // Enter if (!keyCodeIs(e, 13)) { return; diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js index 2fa3f4fc789..b1227fb3533 100644 --- a/app/assets/javascripts/behaviors/requires_input.js +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -23,7 +23,7 @@ $.fn.requiresInput = function requiresInput() { function requireInput() { // Collect the input values of *all* required fields - const values = Array.from($(fieldSelector, $form)).map(field => field.value); + const values = Array.from($(fieldSelector, $form)).map((field) => field.value); // Disable the button if any required fields are empty if (values.length && values.some(isEmpty)) { diff --git a/app/assets/javascripts/behaviors/secret_values.js b/app/assets/javascripts/behaviors/secret_values.js index 5b92608d536..2f1951c97f9 100644 --- a/app/assets/javascripts/behaviors/secret_values.js +++ b/app/assets/javascripts/behaviors/secret_values.js @@ -30,12 +30,12 @@ export default class SecretValues { updateDom(isRevealed) { const values = this.container.querySelectorAll(this.valueSelector); - values.forEach(value => { + values.forEach((value) => { value.classList.toggle('hide', !isRevealed); }); const placeholders = this.container.querySelectorAll(this.placeholderSelector); - placeholders.forEach(placeholder => { + placeholders.forEach((placeholder) => { placeholder.classList.toggle('hide', isRevealed); }); diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js index bbcc40ab9fe..10832583783 100644 --- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js +++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js @@ -49,10 +49,10 @@ export const keybindingGroups = [ // For each keybinding object, add a `customKeys` property populated with the // user's custom keybindings (if the command has been customized). // `customKeys` will be `undefined` if the command hasn't been customized. - .map(group => { + .map((group) => { return { ...group, - keybindings: group.keybindings.map(binding => ({ + keybindings: group.keybindings.map((binding) => ({ ...binding, customKeys: customizations[binding.command], })), @@ -66,7 +66,7 @@ export const keybindingGroups = [ * @example * { "globalShortcuts.togglePerformanceBar": ["p e r f"] } */ -const commandToKeys = flatten(keybindingGroups.map(group => group.keybindings)).reduce( +const commandToKeys = flatten(keybindingGroups.map((group) => group.keybindings)).reduce( (acc, binding) => { acc[binding.command] = binding.customKeys || binding.defaultKeys; return acc; @@ -87,7 +87,7 @@ const commandToKeys = flatten(keybindingGroups.map(group => group.keybindings)). * * Mousetrap.bind(keysFor(TOGGLE_PERFORMANCE_BAR), handler); */ -export const keysFor = command => { +export const keysFor = (command) => { if (shouldDisableShortcuts()) { return []; } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js index c0f67923191..50d2399b312 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js @@ -98,9 +98,7 @@ export default class Shortcuts { }); // eslint-disable-next-line @gitlab/no-global-event-off - $('.js-shortcuts-modal-trigger') - .off('click') - .on('click', this.onToggleHelp); + $('.js-shortcuts-modal-trigger').off('click').on('click', this.onToggleHelp); if (shouldDisableShortcuts()) { disableShortcuts(); @@ -199,7 +197,7 @@ export default class Shortcuts { $textarea.data(LOCAL_MOUSETRAP_DATA_KEY, localMousetrap); toolbarBtnToShortcutsMap.forEach((keyboardShortcuts, $toolbarBtn) => { - localMousetrap.bind(keyboardShortcuts, e => { + localMousetrap.bind(keyboardShortcuts, (e) => { e.preventDefault(); handler($toolbarBtn); @@ -233,7 +231,7 @@ export default class Shortcuts { const localMousetrap = $textarea.data(LOCAL_MOUSETRAP_DATA_KEY); if (localMousetrap) { - getToolbarBtnToShortcutsMap($textarea).forEach(keyboardShortcuts => { + getToolbarBtnToShortcutsMap($textarea).forEach((keyboardShortcuts) => { localMousetrap.unbind(keyboardShortcuts); }); } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js index c35a073b291..11b4fcd4e1c 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js @@ -53,7 +53,7 @@ export default class ShortcutsBlob extends Shortcuts { shortcircuitPermalinkButton() { const button = this.options.fileBlobPermalinkUrlElement; - const handleButton = e => { + const handleButton = (e) => { if (!eventHasModifierKeys(e)) { e.preventDefault(); this.moveToFilePermalink(); diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js index 5a5a67334d3..5e8ddeb6af7 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js @@ -39,7 +39,7 @@ export default class ShortcutsIssuable extends Shortcuts { // ... Or come from a message if (!foundMessage) { if (documentFragment.originalNodes) { - documentFragment.originalNodes.forEach(e => { + documentFragment.originalNodes.forEach((e) => { let node = e; do { // Text nodes don't define the `matches` method @@ -62,7 +62,7 @@ export default class ShortcutsIssuable extends Shortcuts { const blockquoteEl = document.createElement('blockquote'); blockquoteEl.appendChild(el); CopyAsGFM.nodeToGFM(blockquoteEl) - .then(text => { + .then((text) => { if (text.trim() === '') { return false; } diff --git a/app/assets/javascripts/blob/3d_viewer/index.js b/app/assets/javascripts/blob/3d_viewer/index.js index 2d4f45cc365..94397d70384 100644 --- a/app/assets/javascripts/blob/3d_viewer/index.js +++ b/app/assets/javascripts/blob/3d_viewer/index.js @@ -68,7 +68,7 @@ export default class Renderer { } loadFile() { - this.loader.load(this.container.dataset.endpoint, geo => { + this.loader.load(this.container.dataset.endpoint, (geo) => { const obj = new MeshObject(geo); this.objects.push(obj); @@ -99,7 +99,7 @@ export default class Renderer { } changeObjectMaterials(type) { - this.objects.forEach(obj => { + this.objects.forEach((obj) => { obj.changeMaterial(type); }); } diff --git a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js index 5b781947d55..ebe2c2b3bb8 100644 --- a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js +++ b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js @@ -28,7 +28,7 @@ class BalsamiqViewer { .then(({ data }) => { this.renderFile(data); }) - .catch(e => { + .catch((e) => { throw new Error(e); }); } @@ -39,7 +39,7 @@ class BalsamiqViewer { this.initDatabase(fileBuffer); const previews = this.getPreviews(); - previews.forEach(preview => { + previews.forEach((preview) => { const renderedPreview = this.renderPreview(preview); container.appendChild(renderedPreview); diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index e8772b7240a..19bad64155d 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -43,16 +43,14 @@ export default class BlobFileDropzone { previewsContainer: '.dropzone-previews', headers: csrf.headers, init() { - this.on('processing', function() { + this.on('processing', function () { this.options.url = form.attr('action'); }); this.on('addedfile', () => { toggleLoading(submitButton, submitButtonLoadingIcon, false); dropzoneMessage.addClass(HIDDEN_CLASS); - $('.dropzone-alerts') - .html('') - .hide(); + $('.dropzone-alerts').html('').hide(); }); this.on('removedfile', () => { toggleLoading(submitButton, submitButtonLoadingIcon, false); @@ -62,7 +60,7 @@ export default class BlobFileDropzone { $('#modal-upload-blob').modal('hide'); visitUrl(response.filePath); }); - this.on('maxfilesexceeded', function(file) { + this.on('maxfilesexceeded', function (file) { dropzoneMessage.addClass(HIDDEN_CLASS); this.removeFile(file); }); @@ -74,9 +72,7 @@ export default class BlobFileDropzone { }, // Override behavior of adding error underneath preview error(file, errorMessage) { - const stripped = $('<div/>') - .html(errorMessage) - .text(); + const stripped = $('<div/>').html(errorMessage).text(); $('.dropzone-alerts') .html(sprintf(__('Error uploading file: %{stripped}'), { stripped })) .show(); @@ -84,7 +80,7 @@ export default class BlobFileDropzone { }, }); - submitButton.on('click', e => { + submitButton.on('click', (e) => { e.preventDefault(); e.stopPropagation(); if (dropzone[0].dropzone.getQueuedFiles().length === 0) { diff --git a/app/assets/javascripts/blob/blob_line_permalink_updater.js b/app/assets/javascripts/blob/blob_line_permalink_updater.js index 62f0a56ed75..11089b299c5 100644 --- a/app/assets/javascripts/blob/blob_line_permalink_updater.js +++ b/app/assets/javascripts/blob/blob_line_permalink_updater.js @@ -2,12 +2,12 @@ import { getLocationHash } from '../lib/utils/url_utility'; const lineNumberRe = /^L[0-9]+/; -const updateLineNumbersOnBlobPermalinks = linksToUpdate => { +const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => { const hash = getLocationHash(); if (hash && lineNumberRe.test(hash)) { const hashUrlString = `#${hash}`; - [].concat(Array.prototype.slice.call(linksToUpdate)).forEach(permalinkButton => { + [].concat(Array.prototype.slice.call(linksToUpdate)).forEach((permalinkButton) => { const baseHref = permalinkButton.getAttribute('data-original-href') || (() => { @@ -28,7 +28,7 @@ function BlobLinePermalinkUpdater(blobContentHolder, lineNumberSelector, element }, 0); }; - blobContentHolder.addEventListener('click', e => { + blobContentHolder.addEventListener('click', (e) => { if (e.target.matches(lineNumberSelector)) { updateBlameAndBlobPermalinkCb(); } diff --git a/app/assets/javascripts/blob/components/blob_content_error.vue b/app/assets/javascripts/blob/components/blob_content_error.vue index 7344b9cdff5..4c5d9831237 100644 --- a/app/assets/javascripts/blob/components/blob_content_error.vue +++ b/app/assets/javascripts/blob/components/blob_content_error.vue @@ -25,7 +25,7 @@ export default { }, renderErrorReason() { const defaultReasonPath = Object.keys(BLOB_RENDER_ERRORS.REASONS).find( - reason => BLOB_RENDER_ERRORS.REASONS[reason].id === this.viewerError, + (reason) => BLOB_RENDER_ERRORS.REASONS[reason].id === this.viewerError, ); const defaultReason = BLOB_RENDER_ERRORS.REASONS[defaultReasonPath].text; return this.notStoredExternally diff --git a/app/assets/javascripts/blob/components/blob_header_default_actions.vue b/app/assets/javascripts/blob/components/blob_header_default_actions.vue index 6eddec31166..b9f2c5b42e4 100644 --- a/app/assets/javascripts/blob/components/blob_header_default_actions.vue +++ b/app/assets/javascripts/blob/components/blob_header_default_actions.vue @@ -16,6 +16,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + inject: ['blobHash'], props: { rawPath: { type: String, @@ -32,7 +33,6 @@ export default { default: false, }, }, - inject: ['blobHash'], computed: { downloadUrl() { return `${this.rawPath}?inline=false`; @@ -50,7 +50,7 @@ export default { }; </script> <template> - <gl-button-group> + <gl-button-group data-qa-selector="default_actions_container"> <gl-button v-if="!hasRenderError" v-gl-tooltip.hover @@ -59,6 +59,7 @@ export default { :disabled="copyDisabled" :data-clipboard-target="getBlobHashTarget" data-testid="copyContentsButton" + data-qa-selector="copy_contents_button" icon="copy-to-clipboard" category="primary" variant="default" diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js index 8f64bda1ba6..c35f9934004 100644 --- a/app/assets/javascripts/blob/file_template_mediator.js +++ b/app/assets/javascripts/blob/file_template_mediator.js @@ -9,6 +9,7 @@ import { deprecatedCreateFlash as Flash } from '../flash'; import FileTemplateTypeSelector from './template_selectors/type_selector'; import BlobCiYamlSelector from './template_selectors/ci_yaml_selector'; +import BlobCiSyntaxYamlSelector from './template_selectors/ci_syntax_yaml_selector'; import DockerfileSelector from './template_selectors/dockerfile_selector'; import GitignoreSelector from './template_selectors/gitignore_selector'; import LicenseSelector from './template_selectors/license_selector'; @@ -33,24 +34,30 @@ export default class FileTemplateMediator { this.templateSelectors = [ GitignoreSelector, BlobCiYamlSelector, + BlobCiSyntaxYamlSelector, MetricsDashboardSelector, DockerfileSelector, LicenseSelector, - ].map(TemplateSelectorClass => new TemplateSelectorClass({ mediator: this })); + ].map((TemplateSelectorClass) => new TemplateSelectorClass({ mediator: this })); } initTemplateTypeSelector() { this.typeSelector = new FileTemplateTypeSelector({ mediator: this, - dropdownData: this.templateSelectors.map(templateSelector => { - const cfg = templateSelector.config; - - return { - name: cfg.name, - key: cfg.key, - id: cfg.key, - }; - }), + dropdownData: this.templateSelectors + .map((templateSelector) => { + const cfg = templateSelector.config; + + return { + name: cfg.name, + key: cfg.key, + id: cfg.key, + }; + }) + .reduce( + (acc, current) => (acc.find((item) => item.id === current.id) ? acc : [...acc, current]), + [], + ), }); } @@ -92,7 +99,7 @@ export default class FileTemplateMediator { } listenForPreviewMode() { - this.$navLinks.on('click', 'a', e => { + this.$navLinks.on('click', 'a', (e) => { const urlPieces = e.target.href.split('#'); const hash = urlPieces[1]; if (hash === 'preview') { @@ -108,7 +115,7 @@ export default class FileTemplateMediator { e.preventDefault(); } - this.templateSelectors.forEach(selector => { + this.templateSelectors.forEach((selector) => { if (selector.config.key === item.key) { selector.show(); } else { @@ -131,7 +138,7 @@ export default class FileTemplateMediator { selector.renderLoading(); this.fetchFileTemplate(selector.config.type, query, data) - .then(file => { + .then((file) => { this.setEditorContent(file); this.setFilename(name); selector.renderLoaded(); @@ -150,12 +157,12 @@ export default class FileTemplateMediator { initPopover(suggestCommitChanges); } }) - .catch(err => new Flash(`An error occurred while fetching the template: ${err}`)); + .catch((err) => new Flash(`An error occurred while fetching the template: ${err}`)); } displayMatchedTemplateSelector() { const currentInput = this.getFilename(); - this.templateSelectors.forEach(selector => { + this.templateSelectors.forEach((selector) => { const match = selector.config.pattern.test(currentInput); if (match) { @@ -167,8 +174,8 @@ export default class FileTemplateMediator { } fetchFileTemplate(type, query, data = {}) { - return new Promise(resolve => { - const resolveFile = file => resolve(file); + return new Promise((resolve) => { + const resolveFile = (file) => resolve(file); Api.projectTemplate(this.projectId, type, query, data, resolveFile); }); @@ -187,7 +194,7 @@ export default class FileTemplateMediator { } findTemplateSelectorByKey(key) { - return this.templateSelectors.find(selector => selector.config.key === key); + return this.templateSelectors.find((selector) => selector.config.key === key); } hideTemplateSelectorMenu() { @@ -243,6 +250,6 @@ export default class FileTemplateMediator { } getSelected() { - return this.templateSelectors.find(selector => selector.selected); + return this.templateSelectors.find((selector) => selector.selected); } } diff --git a/app/assets/javascripts/blob/notebook/notebook_viewer.vue b/app/assets/javascripts/blob/notebook/notebook_viewer.vue index ea33d621d47..02f93e14219 100644 --- a/app/assets/javascripts/blob/notebook/notebook_viewer.vue +++ b/app/assets/javascripts/blob/notebook/notebook_viewer.vue @@ -45,12 +45,12 @@ export default { loadFile() { axios .get(this.endpoint) - .then(res => res.data) - .then(data => { + .then((res) => res.data) + .then((data) => { this.json = data; this.loading = false; }) - .catch(e => { + .catch((e) => { if (e.status !== 200) { this.loadError = true; } diff --git a/app/assets/javascripts/blob/openapi/index.js b/app/assets/javascripts/blob/openapi/index.js index 12cc2be8246..e6dc463f764 100644 --- a/app/assets/javascripts/blob/openapi/index.js +++ b/app/assets/javascripts/blob/openapi/index.js @@ -12,7 +12,7 @@ export default () => { dom_id: '#js-openapi-viewer', }); }) - .catch(error => { + .catch((error) => { flash(__('Something went wrong while initializing the OpenAPI viewer')); throw error; }); diff --git a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue index 02a522dda9d..fdaa4b082f7 100644 --- a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue +++ b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue @@ -98,7 +98,7 @@ export default { </template> <p> <gl-sprintf :message="$options.i18n.bodyMessage"> - <template #codeQualityLink="{content}"> + <template #codeQualityLink="{ content }"> <gl-link :href="codeQualityLink" target="_blank" class="font-size-inherit">{{ content }}</gl-link> @@ -106,12 +106,12 @@ export default { </gl-sprintf> </p> <gl-sprintf :message="$options.i18n.helpMessage"> - <template #beginnerLink="{content}"> + <template #beginnerLink="{ content }"> <gl-link :href="$options.beginnerLink" target="_blank"> {{ content }} </gl-link> </template> - <template #exampleLink="{content}"> + <template #exampleLink="{ content }"> <gl-link :href="exampleLink" target="_blank"> {{ content }} </gl-link> diff --git a/app/assets/javascripts/blob/sketch/index.js b/app/assets/javascripts/blob/sketch/index.js index 767e205fcaa..d257810da65 100644 --- a/app/assets/javascripts/blob/sketch/index.js +++ b/app/assets/javascripts/blob/sketch/index.js @@ -12,9 +12,9 @@ export default class SketchLoader { load() { return this.getZipFile() - .then(data => JSZip.loadAsync(data)) - .then(asyncResult => asyncResult.files['previews/preview.png'].async('uint8array')) - .then(content => { + .then((data) => JSZip.loadAsync(data)) + .then((asyncResult) => asyncResult.files['previews/preview.png'].async('uint8array')) + .then((content) => { const url = window.URL || window.webkitURL; const blob = new Blob([new Uint8Array(content)], { type: 'image/png', diff --git a/app/assets/javascripts/blob/stl_viewer.js b/app/assets/javascripts/blob/stl_viewer.js index f129b6e631e..339906adc34 100644 --- a/app/assets/javascripts/blob/stl_viewer.js +++ b/app/assets/javascripts/blob/stl_viewer.js @@ -3,8 +3,8 @@ import Renderer from './3d_viewer'; export default () => { const viewer = new Renderer(document.getElementById('js-stl-viewer')); - [].slice.call(document.querySelectorAll('.js-material-changer')).forEach(el => { - el.addEventListener('click', e => { + [].slice.call(document.querySelectorAll('.js-material-changer')).forEach((el) => { + el.addEventListener('click', (e) => { const { target } = e; e.preventDefault(); diff --git a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/index.js b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/index.js index 55edb852ee6..d0dd80932cc 100644 --- a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/index.js +++ b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/index.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import Popover from './components/popover.vue'; -export default el => +export default (el) => new Vue({ el, render(createElement) { diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js index ae9bb3455f0..9fa70ce3c62 100644 --- a/app/assets/javascripts/blob/template_selector.js +++ b/app/assets/javascripts/blob/template_selector.js @@ -26,12 +26,12 @@ export default class TemplateSelector { data, filterable: true, selectable: true, - toggleLabel: item => item.name, + toggleLabel: (item) => item.name, search: { fields: ['name'], }, - clicked: options => this.onDropdownClicked(options), - text: item => item.name, + clicked: (options) => this.onDropdownClicked(options), + text: (item) => item.name, }); } @@ -46,7 +46,7 @@ export default class TemplateSelector { } listenForFilenameInput() { - return this.$filenameInput.on('keyup blur', e => this.renderMatchedDropdown(e)); + return this.$filenameInput.on('keyup blur', (e) => this.renderMatchedDropdown(e)); } renderMatchedDropdown() { diff --git a/app/assets/javascripts/blob/template_selectors/ci_syntax_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/ci_syntax_yaml_selector.js new file mode 100644 index 00000000000..9370e170571 --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/ci_syntax_yaml_selector.js @@ -0,0 +1,29 @@ +import FileTemplateSelector from '../file_template_selector'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; + +export default class BlobCiSyntaxYamlSelector extends FileTemplateSelector { + constructor({ mediator }) { + super(mediator); + this.config = { + key: 'gitlab-ci-yaml', + name: '.gitlab-ci.yml', + pattern: /(.gitlab-ci.yml)/, + type: 'gitlab_ci_syntax_ymls', + dropdown: '.js-gitlab-ci-syntax-yml-selector', + wrapper: '.js-gitlab-ci-syntax-yml-selector-wrap', + }; + } + + initDropdown() { + initDeprecatedJQueryDropdown(this.$dropdown, { + data: this.$dropdown.data('data'), + filterable: true, + selectable: true, + search: { + fields: ['name'], + }, + clicked: (options) => this.reportSelectionName(options), + text: (item) => item.name, + }); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js index 3a4e86fe572..3879a6c5742 100644 --- a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js +++ b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js @@ -23,8 +23,8 @@ export default class BlobCiYamlSelector extends FileTemplateSelector { search: { fields: ['name'], }, - clicked: options => this.reportSelectionName(options), - text: item => item.name, + clicked: (options) => this.reportSelectionName(options), + text: (item) => item.name, }); } } diff --git a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js index 3cb4bb83930..5d976c5acdb 100644 --- a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js +++ b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js @@ -24,8 +24,8 @@ export default class DockerfileSelector extends FileTemplateSelector { search: { fields: ['name'], }, - clicked: options => this.reportSelectionName(options), - text: item => item.name, + clicked: (options) => this.reportSelectionName(options), + text: (item) => item.name, }); } } diff --git a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js index 1721230dcb7..1bb1cbb74de 100644 --- a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js +++ b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js @@ -22,8 +22,8 @@ export default class BlobGitignoreSelector extends FileTemplateSelector { search: { fields: ['name'], }, - clicked: options => this.reportSelectionName(options), - text: item => item.name, + clicked: (options) => this.reportSelectionName(options), + text: (item) => item.name, }); } } diff --git a/app/assets/javascripts/blob/template_selectors/license_selector.js b/app/assets/javascripts/blob/template_selectors/license_selector.js index dafde82b1e0..affa20997e9 100644 --- a/app/assets/javascripts/blob/template_selectors/license_selector.js +++ b/app/assets/javascripts/blob/template_selectors/license_selector.js @@ -22,7 +22,7 @@ export default class BlobLicenseSelector extends FileTemplateSelector { search: { fields: ['name'], }, - clicked: options => { + clicked: (options) => { const { e } = options; const el = options.$el; const query = options.selectedObj; @@ -39,7 +39,7 @@ export default class BlobLicenseSelector extends FileTemplateSelector { data, }); }, - text: item => item.name, + text: (item) => item.name, }); } } diff --git a/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js b/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js index 9e698bfea5d..42adab05ce3 100644 --- a/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js +++ b/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js @@ -22,8 +22,8 @@ export default class MetricsDashboardSelector extends FileTemplateSelector { search: { fields: ['name'], }, - clicked: options => this.reportSelectionName(options), - text: item => item.name, + clicked: (options) => this.reportSelectionName(options), + text: (item) => item.name, }); } } diff --git a/app/assets/javascripts/blob/template_selectors/type_selector.js b/app/assets/javascripts/blob/template_selectors/type_selector.js index 01625911815..f74f7535d99 100644 --- a/app/assets/javascripts/blob/template_selectors/type_selector.js +++ b/app/assets/javascripts/blob/template_selectors/type_selector.js @@ -17,8 +17,8 @@ export default class FileTemplateTypeSelector extends FileTemplateSelector { data: this.config.dropdownData, filterable: false, selectable: true, - clicked: options => this.mediator.selectTemplateTypeOptions(options), - text: item => item.name, + clicked: (options) => this.mediator.selectTemplateTypeOptions(options), + text: (item) => item.name, }); } } diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index 01350acad0c..4e6ec20ec64 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -7,7 +7,7 @@ import eventHub from '../../notes/event_hub'; import { __ } from '~/locale'; import { fixTitle } from '~/tooltips'; -const loadRichBlobViewer = type => { +const loadRichBlobViewer = (type) => { switch (type) { case 'balsamiq': return import(/* webpackChunkName: 'balsamiq_viewer' */ '../balsamiq_viewer'); @@ -30,8 +30,8 @@ export const handleBlobRichViewer = (viewer, type) => { if (!viewer || !type) return; loadRichBlobViewer(type) - .then(module => module?.default(viewer)) - .catch(error => { + .then((module) => module?.default(viewer)) + .catch((error) => { Flash(__('Error loading file viewer.')); throw error; }); @@ -84,7 +84,7 @@ export default class BlobViewer { initBindings() { if (this.switcherBtns.length) { - Array.from(this.switcherBtns).forEach(el => { + Array.from(this.switcherBtns).forEach((el) => { el.addEventListener('click', this.switchViewHandler.bind(this)); }); } @@ -155,7 +155,7 @@ export default class BlobViewer { this.toggleCopyButtonState(); BlobViewer.loadViewer(newViewer) - .then(viewer => { + .then((viewer) => { $(viewer).renderGFM(); this.$fileHolder.trigger('highlight:line'); diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index 678044687a9..7c2217a59e9 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -75,7 +75,7 @@ export default () => { }); initPopovers(); }) - .catch(e => createFlash(e)); + .catch((e) => createFlash(e)); cancelLink.on('click', () => { window.onbeforeunload = null; diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index 1bc51aa1d6f..c7f66a357f3 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -5,7 +5,7 @@ import { BLOB_EDITOR_ERROR, BLOB_PREVIEW_ERROR } from './constants'; import TemplateSelectorMediator from '../blob/file_template_mediator'; import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown'; import EditorLite from '~/editor/editor_lite'; -import { FileTemplateExtension } from '~/editor/editor_file_template_ext'; +import { FileTemplateExtension } from '~/editor/extensions/editor_file_template_ext'; import { insertFinalNewline } from '~/lib/utils/text_utility'; export default class EditBlob { @@ -16,12 +16,12 @@ export default class EditBlob { this.configureMonacoEditor(); if (this.options.isMarkdown) { - import('~/editor/editor_markdown_ext') + import('~/editor/extensions/editor_markdown_ext') .then(({ EditorMarkdownExtension: MarkdownExtension } = {}) => { this.editor.use(new MarkdownExtension()); addEditorMarkdownListeners(this.editor); }) - .catch(e => createFlash(`${BLOB_EDITOR_ERROR}: ${e}`)); + .catch((e) => createFlash(`${BLOB_EDITOR_ERROR}: ${e}`)); } this.initModePanesAndLinks(); @@ -66,7 +66,7 @@ export default class EditBlob { initModePanesAndLinks() { this.$editModePanes = $('.js-edit-mode-pane'); this.$editModeLinks = $('.js-edit-mode a'); - this.$editModeLinks.on('click', e => this.editModeLinkClickHandler(e)); + this.$editModeLinks.on('click', (e) => this.editModeLinkClickHandler(e)); } editModeLinkClickHandler(e) { diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js index e5ff41dab74..965d3571f42 100644 --- a/app/assets/javascripts/boards/boards_util.js +++ b/app/assets/javascripts/boards/boards_util.js @@ -1,5 +1,4 @@ import { sortBy } from 'lodash'; -import axios from '~/lib/utils/axios_utils'; import { ListType } from './constants'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; @@ -42,14 +41,14 @@ export function formatListIssues(listIssues) { const listData = listIssues.nodes.reduce((map, list) => { listIssuesCount = list.issues.count; - let sortedIssues = list.issues.edges.map(issueNode => ({ + let sortedIssues = list.issues.edges.map((issueNode) => ({ ...issueNode.node, })); sortedIssues = sortBy(sortedIssues, 'relativePosition'); return { ...map, - [list.id]: sortedIssues.map(i => { + [list.id]: sortedIssues.map((i) => { const id = getIdFromGraphQLId(i.id); const listIssue = { @@ -83,49 +82,64 @@ export function fullBoardId(boardId) { return `gid://gitlab/Board/${boardId}`; } +export function fullIterationId(id) { + return `gid://gitlab/Iteration/${id}`; +} + +export function fullUserId(id) { + return `gid://gitlab/User/${id}`; +} + +export function fullMilestoneId(id) { + return `gid://gitlab/Milestone/${id}`; +} + export function fullLabelId(label) { - if (label.project_id !== null) { + if (label.project_id && label.project_id !== null) { return `gid://gitlab/ProjectLabel/${label.id}`; } return `gid://gitlab/GroupLabel/${label.id}`; } +export function formatIssueInput(issueInput, boardConfig) { + const { labelIds = [], assigneeIds = [] } = issueInput; + const { labels, assigneeId, milestoneId } = boardConfig; + + return { + milestoneId: milestoneId ? fullMilestoneId(milestoneId) : null, + ...issueInput, + labelIds: [...labelIds, ...(labels?.map((l) => fullLabelId(l)) || [])], + assigneeIds: [...assigneeIds, ...(assigneeId ? [fullUserId(assigneeId)] : [])], + }; +} + export function moveIssueListHelper(issue, fromList, toList) { const updatedIssue = issue; if ( toList.listType === ListType.label && - !updatedIssue.labels.find(label => label.id === toList.label.id) + !updatedIssue.labels.find((label) => label.id === toList.label.id) ) { updatedIssue.labels.push(toList.label); } if (fromList?.label && fromList.listType === ListType.label) { - updatedIssue.labels = updatedIssue.labels.filter(label => fromList.label.id !== label.id); + updatedIssue.labels = updatedIssue.labels.filter((label) => fromList.label.id !== label.id); } if ( toList.listType === ListType.assignee && - !updatedIssue.assignees.find(assignee => assignee.id === toList.assignee.id) + !updatedIssue.assignees.find((assignee) => assignee.id === toList.assignee.id) ) { updatedIssue.assignees.push(toList.assignee); } if (fromList?.assignee && fromList.listType === ListType.assignee) { updatedIssue.assignees = updatedIssue.assignees.filter( - assignee => assignee.id !== fromList.assignee.id, + (assignee) => assignee.id !== fromList.assignee.id, ); } return updatedIssue; } -export function getBoardsPath(endpoint, board) { - const path = `${endpoint}${board.id ? `/${board.id}` : ''}.json`; - - if (board.id) { - return axios.put(path, { board }); - } - return axios.post(path, { board }); -} - export function isListDraggable(list) { return list.listType !== ListType.backlog && list.listType !== ListType.closed; } @@ -141,6 +155,6 @@ export default { formatListIssues, fullBoardId, fullLabelId, - getBoardsPath, + fullIterationId, isListDraggable, }; diff --git a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue index 1469efae5a6..5d381f9a570 100644 --- a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue +++ b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue @@ -119,7 +119,7 @@ export default { this.selected = this.selected.concat(name); }, unselect(name) { - this.selected = this.selected.filter(user => user.username !== name); + this.selected = this.selected.filter((user) => user.username !== name); }, saveAssignees() { this.setAssignees(this.selectedUserNames); diff --git a/app/assets/javascripts/boards/components/board_card_layout.vue b/app/assets/javascripts/boards/components/board_card_layout.vue index f796acd2303..0a2301394c1 100644 --- a/app/assets/javascripts/boards/components/board_card_layout.vue +++ b/app/assets/javascripts/boards/components/board_card_layout.vue @@ -1,12 +1,17 @@ <script> +import { mapActions, mapGetters } from 'vuex'; import IssueCardInner from './issue_card_inner.vue'; +import IssueCardInnerDeprecated from './issue_card_inner_deprecated.vue'; import boardsStore from '../stores/boards_store'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { ISSUABLE } from '~/boards/constants'; export default { - name: 'BoardsIssueCard', + name: 'BoardCardLayout', components: { - IssueCardInner, + IssueCardInner: gon.features?.graphqlBoardLists ? IssueCardInner : IssueCardInnerDeprecated, }, + mixins: [glFeatureFlagMixin()], props: { list: { type: Object, @@ -41,11 +46,13 @@ export default { }; }, computed: { + ...mapGetters(['isSwimlanesOn']), multiSelectVisible() { - return this.multiSelect.list.findIndex(issue => issue.id === this.issue.id) > -1; + return this.multiSelect.list.findIndex((issue) => issue.id === this.issue.id) > -1; }, }, methods: { + ...mapActions(['setActiveId']), mouseDown() { this.showDetail = true; }, @@ -56,6 +63,11 @@ export default { // Don't do anything if this happened on a no trigger element if (e.target.classList.contains('js-no-trigger')) return; + if (this.glFeatures.graphqlBoardLists || this.isSwimlanesOn) { + this.setActiveId({ id: this.issue.id, sidebarType: ISSUABLE }); + return; + } + const isMultiSelect = e.ctrlKey || e.metaKey; if (this.showDetail || isMultiSelect) { @@ -80,7 +92,7 @@ export default { :data-issue-iid="issue.iid" :data-issue-path="issue.referencePath" data-testid="board_card" - class="board-card p-3 rounded" + class="board-card gl-p-5 gl-rounded-base" @mousedown="mouseDown" @mousemove="mouseMove" @mouseup="showIssue($event)" diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue index 753e6941c43..9f0eef844f6 100644 --- a/app/assets/javascripts/boards/components/board_column.vue +++ b/app/assets/javascripts/boards/components/board_column.vue @@ -1,16 +1,19 @@ <script> -// This component is being replaced in favor of './board_column_new.vue' for GraphQL boards -import Sortable from 'sortablejs'; +import { mapGetters, mapActions, mapState } from 'vuex'; import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue'; import BoardList from './board_list.vue'; -import boardsStore from '../stores/boards_store'; -import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options'; +import { isListDraggable } from '../boards_util'; export default { components: { BoardListHeader, BoardList, }, + inject: { + boardId: { + default: '', + }, + }, props: { list: { type: Object, @@ -27,58 +30,27 @@ export default { default: false, }, }, - inject: { - boardId: { - default: '', - }, - }, - data() { - return { - detailIssue: boardsStore.detail, - filter: boardsStore.filter, - }; - }, computed: { + ...mapState(['filterParams']), + ...mapGetters(['getIssuesByList']), listIssues() { - return this.list.issues; + return this.getIssuesByList(this.list.id); + }, + isListDraggable() { + return isListDraggable(this.list); }, }, watch: { - filter: { + filterParams: { handler() { - this.list.page = 1; - this.list.getIssues(true).catch(() => { - // TODO: handle request error - }); + this.fetchIssuesForList({ listId: this.list.id }); }, deep: true, + immediate: true, }, }, - mounted() { - const instance = this; - - const sortableOptions = getBoardSortableDefaultOptions({ - disabled: this.disabled, - group: 'boards', - draggable: '.is-draggable', - handle: '.js-board-handle', - onEnd(e) { - sortableEnd(); - - const sortable = this; - - if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { - const order = sortable.toArray(); - const list = boardsStore.findList('id', parseInt(e.item.dataset.id, 10)); - - instance.$nextTick(() => { - boardsStore.moveList(list, order); - }); - } - }, - }); - - Sortable.create(this.$el.parentNode, sortableOptions); + methods: { + ...mapActions(['fetchIssuesForList']), }, }; </script> @@ -86,20 +58,25 @@ export default { <template> <div :class="{ - 'is-draggable': !list.preset, - 'is-expandable': list.isExpandable, - 'is-collapsed': !list.isExpanded, - 'board-type-assignee': list.type === 'assignee', + 'is-draggable': isListDraggable, + 'is-collapsed': list.collapsed, + 'board-type-assignee': list.listType === 'assignee', }" :data-id="list.id" - class="board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal" + class="board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal is-expandable" data-qa-selector="board_list" > <div class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base" > <board-list-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" /> - <board-list ref="board-list" :disabled="disabled" :issues="listIssues" :list="list" /> + <board-list + ref="board-list" + :disabled="disabled" + :issues="listIssues" + :list="list" + :can-admin-list="canAdminList" + /> </div> </div> </template> diff --git a/app/assets/javascripts/boards/components/board_column_deprecated.vue b/app/assets/javascripts/boards/components/board_column_deprecated.vue new file mode 100644 index 00000000000..35688efceb4 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_column_deprecated.vue @@ -0,0 +1,105 @@ +<script> +// This component is being replaced in favor of './board_column.vue' for GraphQL boards +import Sortable from 'sortablejs'; +import BoardListHeader from 'ee_else_ce/boards/components/board_list_header_deprecated.vue'; +import BoardList from './board_list_deprecated.vue'; +import boardsStore from '../stores/boards_store'; +import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options'; + +export default { + components: { + BoardListHeader, + BoardList, + }, + inject: { + boardId: { + default: '', + }, + }, + props: { + list: { + type: Object, + default: () => ({}), + required: false, + }, + disabled: { + type: Boolean, + required: true, + }, + canAdminList: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + detailIssue: boardsStore.detail, + filter: boardsStore.filter, + }; + }, + computed: { + listIssues() { + return this.list.issues; + }, + }, + watch: { + filter: { + handler() { + this.list.page = 1; + this.list.getIssues(true).catch(() => { + // TODO: handle request error + }); + }, + deep: true, + }, + }, + mounted() { + const instance = this; + + const sortableOptions = getBoardSortableDefaultOptions({ + disabled: this.disabled, + group: 'boards', + draggable: '.is-draggable', + handle: '.js-board-handle', + onEnd(e) { + sortableEnd(); + + const sortable = this; + + if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { + const order = sortable.toArray(); + const list = boardsStore.findList('id', parseInt(e.item.dataset.id, 10)); + + instance.$nextTick(() => { + boardsStore.moveList(list, order); + }); + } + }, + }); + + Sortable.create(this.$el.parentNode, sortableOptions); + }, +}; +</script> + +<template> + <div + :class="{ + 'is-draggable': !list.preset, + 'is-expandable': list.isExpandable, + 'is-collapsed': !list.isExpanded, + 'board-type-assignee': list.type === 'assignee', + }" + :data-id="list.id" + class="board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal" + data-qa-selector="board_list" + > + <div + class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base" + > + <board-list-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" /> + <board-list ref="board-list" :disabled="disabled" :issues="listIssues" :list="list" /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/board_column_new.vue b/app/assets/javascripts/boards/components/board_column_new.vue deleted file mode 100644 index 7839f45c48b..00000000000 --- a/app/assets/javascripts/boards/components/board_column_new.vue +++ /dev/null @@ -1,82 +0,0 @@ -<script> -import { mapGetters, mapActions, mapState } from 'vuex'; -import BoardListHeader from 'ee_else_ce/boards/components/board_list_header_new.vue'; -import BoardList from './board_list_new.vue'; -import { isListDraggable } from '../boards_util'; - -export default { - components: { - BoardListHeader, - BoardList, - }, - props: { - list: { - type: Object, - default: () => ({}), - required: false, - }, - disabled: { - type: Boolean, - required: true, - }, - canAdminList: { - type: Boolean, - required: false, - default: false, - }, - }, - inject: { - boardId: { - default: '', - }, - }, - computed: { - ...mapState(['filterParams']), - ...mapGetters(['getIssuesByList']), - listIssues() { - return this.getIssuesByList(this.list.id); - }, - isListDraggable() { - return isListDraggable(this.list); - }, - }, - watch: { - filterParams: { - handler() { - this.fetchIssuesForList({ listId: this.list.id }); - }, - deep: true, - immediate: true, - }, - }, - methods: { - ...mapActions(['fetchIssuesForList']), - }, -}; -</script> - -<template> - <div - :class="{ - 'is-draggable': isListDraggable, - 'is-collapsed': list.collapsed, - 'board-type-assignee': list.listType === 'assignee', - }" - :data-id="list.id" - class="board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal is-expandable" - data-qa-selector="board_list" - > - <div - class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base" - > - <board-list-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" /> - <board-list - ref="board-list" - :disabled="disabled" - :issues="listIssues" - :list="list" - :can-admin-list="canAdminList" - /> - </div> - </div> -</template> diff --git a/app/assets/javascripts/boards/components/board_configuration_options.vue b/app/assets/javascripts/boards/components/board_configuration_options.vue index 99d1e4a2611..b8ee930a8c9 100644 --- a/app/assets/javascripts/boards/components/board_configuration_options.vue +++ b/app/assets/javascripts/boards/components/board_configuration_options.vue @@ -6,36 +6,13 @@ export default { GlFormCheckbox, }, props: { - currentBoard: { - type: Object, - required: true, - }, - board: { - type: Object, + hideBacklogList: { + type: Boolean, required: true, }, - isNewForm: { + hideClosedList: { type: Boolean, - required: false, - default: false, - }, - }, - data() { - const { hide_backlog_list: hideBacklogList, hide_closed_list: hideClosedList } = this.isNewForm - ? this.board - : this.currentBoard; - - return { - hideClosedList, - hideBacklogList, - }; - }, - methods: { - changeClosedList(checked) { - this.board.hideClosedList = !checked; - }, - changeBacklogList(checked) { - this.board.hideBacklogList = !checked; + required: true, }, }, }; @@ -52,13 +29,13 @@ export default { <gl-form-checkbox :checked="!hideBacklogList" data-testid="backlog-list-checkbox" - @change="changeBacklogList" + @change="$emit('update:hideBacklogList', !hideBacklogList)" >{{ __('Show the Open list') }} </gl-form-checkbox> <gl-form-checkbox :checked="!hideClosedList" data-testid="closed-list-checkbox" - @change="changeClosedList" + @change="$emit('update:hideClosedList', !hideClosedList)" >{{ __('Show the Closed list') }} </gl-form-checkbox> </div> diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue index b366aa6fdb3..19254343208 100644 --- a/app/assets/javascripts/boards/components/board_content.vue +++ b/app/assets/javascripts/boards/components/board_content.vue @@ -3,15 +3,15 @@ import Draggable from 'vuedraggable'; import { mapState, mapGetters, mapActions } from 'vuex'; import { sortBy } from 'lodash'; import { GlAlert } from '@gitlab/ui'; +import BoardColumnDeprecated from './board_column_deprecated.vue'; import BoardColumn from './board_column.vue'; -import BoardColumnNew from './board_column_new.vue'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import defaultSortableConfig from '~/sortable/sortable_config'; import { sortableEnd, sortableStart } from '~/boards/mixins/sortable_default_options'; export default { components: { - BoardColumn: gon.features?.graphqlBoardLists ? BoardColumnNew : BoardColumn, + BoardColumn: gon.features?.graphqlBoardLists ? BoardColumn : BoardColumnDeprecated, BoardContentSidebar: () => import('ee_component/boards/components/board_content_sidebar.vue'), EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'), GlAlert, @@ -20,7 +20,8 @@ export default { props: { lists: { type: Array, - required: true, + required: false, + default: () => [], }, canAdminList: { type: Boolean, @@ -53,7 +54,7 @@ export default { fallbackOnBody: false, group: 'boards-list', tag: 'div', - value: this.lists, + value: this.boardListsToUse, }; return this.canDragColumns ? options : {}; @@ -108,14 +109,14 @@ export default { /> </component> - <template v-else> - <epics-swimlanes - ref="swimlanes" - :lists="boardListsToUse" - :can-admin-list="canAdminList" - :disabled="disabled" - /> - <board-content-sidebar /> - </template> + <epics-swimlanes + v-else + ref="swimlanes" + :lists="boardListsToUse" + :can-admin-list="canAdminList" + :disabled="disabled" + /> + + <board-content-sidebar v-if="isSwimlanesOn || glFeatures.graphqlBoardLists" /> </div> </template> diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue index dab934352ca..c701ecd3040 100644 --- a/app/assets/javascripts/boards/components/board_form.vue +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -1,20 +1,24 @@ <script> import { GlModal } from '@gitlab/ui'; -import { pick } from 'lodash'; import { __, s__ } from '~/locale'; import { deprecatedCreateFlash as Flash } from '~/flash'; import { visitUrl } from '~/lib/utils/url_utility'; +import { getParameterByName } from '~/lib/utils/common_utils'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; import boardsStore from '~/boards/stores/boards_store'; -import { fullBoardId, getBoardsPath } from '../boards_util'; +import { fullLabelId, fullBoardId } from '../boards_util'; import BoardConfigurationOptions from './board_configuration_options.vue'; -import createBoardMutation from '../graphql/board.mutation.graphql'; +import updateBoardMutation from '../graphql/board_update.mutation.graphql'; +import createBoardMutation from '../graphql/board_create.mutation.graphql'; +import destroyBoardMutation from '../graphql/board_destroy.mutation.graphql'; const boardDefaults = { id: false, name: '', labels: [], milestone_id: undefined, + iteration_id: undefined, assignee: {}, assignee_id: undefined, weight: null, @@ -46,6 +50,14 @@ export default { GlModal, BoardConfigurationOptions, }, + inject: { + fullPath: { + default: '', + }, + rootPath: { + default: '', + }, + }, props: { canAdminBoard: { type: Boolean, @@ -89,11 +101,6 @@ export default { required: true, }, }, - inject: { - endpoints: { - default: {}, - }, - }, data() { return { board: { ...boardDefaults, ...this.currentBoard }, @@ -154,14 +161,44 @@ export default { text: this.$options.i18n.cancelButtonText, }; }, - boardPayload() { - const { assignee, milestone, labels } = this.board; - return { - ...this.board, - assignee_id: assignee?.id, - milestone_id: milestone?.id, - label_ids: labels.length ? labels.map(b => b.id) : [''], + currentMutation() { + return this.board.id ? updateBoardMutation : createBoardMutation; + }, + mutationVariables() { + const { board } = this; + /* eslint-disable @gitlab/require-i18n-strings */ + let baseMutationVariables = { + name: board.name, + hideBacklogList: board.hide_backlog_list, + hideClosedList: board.hide_closed_list, }; + + if (this.scopedIssueBoardFeatureEnabled) { + baseMutationVariables = { + ...baseMutationVariables, + weight: board.weight, + assigneeId: board.assignee?.id ? convertToGraphQLId('User', board.assignee.id) : null, + milestoneId: + board.milestone?.id || board.milestone?.id === 0 + ? convertToGraphQLId('Milestone', board.milestone.id) + : null, + labelIds: board.labels.map(fullLabelId), + iterationId: board.iteration_id + ? convertToGraphQLId('Iteration', board.iteration_id) + : null, + }; + } + /* eslint-enable @gitlab/require-i18n-strings */ + return board.id + ? { + ...baseMutationVariables, + id: fullBoardId(board.id), + } + : { + ...baseMutationVariables, + projectPath: this.projectId ? this.fullPath : null, + groupPath: this.groupId ? this.fullPath : null, + }; }, }, mounted() { @@ -171,55 +208,51 @@ export default { } }, methods: { - callBoardMutation(id) { - return this.$apollo.mutate({ - mutation: createBoardMutation, - variables: { - ...pick(this.boardPayload, ['hideClosedList', 'hideBacklogList']), - id, - }, - }); + setIteration(iterationId) { + this.board.iteration_id = iterationId; }, - async updateBoard() { - const responses = await Promise.all([ - // Remove unnecessary REST API call when https://gitlab.com/gitlab-org/gitlab/-/issues/282299#note_462996301 is resolved - getBoardsPath(this.endpoints.boardsEndpoint, this.boardPayload), - this.callBoardMutation(fullBoardId(this.boardPayload.id)), - ]); + async createOrUpdateBoard() { + const response = await this.$apollo.mutate({ + mutation: this.currentMutation, + variables: { input: this.mutationVariables }, + }); - return responses[0].data; - }, - async createBoard() { - // TODO: change this to use `createBoard` mutation https://gitlab.com/gitlab-org/gitlab/-/issues/292466 is resolved - const boardData = await getBoardsPath(this.endpoints.boardsEndpoint, this.boardPayload); - this.callBoardMutation(fullBoardId(boardData.data.id)); + if (!this.board.id) { + return response.data.createBoard.board.webPath; + } - return boardData.data || boardData; + const path = response.data.updateBoard.board.webPath; + const param = getParameterByName('group_by') + ? `?group_by=${getParameterByName('group_by')}` + : ''; + return `${path}${param}`; }, - submit() { + async submit() { if (this.board.name.length === 0) return; this.isLoading = true; if (this.isDeleteForm) { - boardsStore - .deleteBoard(this.currentBoard) - .then(() => { - this.isLoading = false; - visitUrl(boardsStore.rootPath); - }) - .catch(() => { - Flash(this.$options.i18n.deleteErrorMessage); - this.isLoading = false; + try { + await this.$apollo.mutate({ + mutation: destroyBoardMutation, + variables: { + id: fullBoardId(this.board.id), + }, }); + visitUrl(this.rootPath); + } catch { + Flash(this.$options.i18n.deleteErrorMessage); + } finally { + this.isLoading = false; + } } else { - const boardAction = this.boardPayload.id ? this.updateBoard : this.createBoard; - boardAction() - .then(data => { - visitUrl(data.board_path); - }) - .catch(() => { - Flash(this.$options.i18n.saveErrorMessage); - this.isLoading = false; - }); + try { + const url = await this.createOrUpdateBoard(); + visitUrl(url); + } catch { + Flash(this.$options.i18n.saveErrorMessage); + } finally { + this.isLoading = false; + } } }, cancel() { @@ -273,9 +306,8 @@ export default { </div> <board-configuration-options - :is-new-form="isNewForm" - :board="board" - :current-board="currentBoard" + :hide-backlog-list.sync="board.hide_backlog_list" + :hide-closed-list.sync="board.hide_closed_list" /> <board-scope @@ -289,6 +321,7 @@ export default { :project-id="projectId" :group-id="groupId" :weights="weights" + @set-iteration="setIteration" /> </form> </gl-modal> diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 1f87b563e73..b6e4d0980fa 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -1,27 +1,24 @@ <script> -import { Sortable, MultiDrag } from 'sortablejs'; +import Draggable from 'vuedraggable'; +import { mapActions, mapState } from 'vuex'; import { GlLoadingIcon } from '@gitlab/ui'; -import boardNewIssue from './board_new_issue.vue'; -import boardCard from './board_card.vue'; +import defaultSortableConfig from '~/sortable/sortable_config'; +import { sortableStart, sortableEnd } from '~/boards/mixins/sortable_default_options'; +import BoardNewIssue from './board_new_issue.vue'; +import BoardCard from './board_card.vue'; import eventHub from '../eventhub'; -import boardsStore from '../stores/boards_store'; import { sprintf, __ } from '~/locale'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; -import { - getBoardSortableDefaultOptions, - sortableStart, - sortableEnd, -} from '../mixins/sortable_default_options'; - -// This component is being replaced in favor of './board_list_new.vue' for GraphQL boards - -Sortable.mount(new MultiDrag()); export default { name: 'BoardList', + i18n: { + loadingIssues: __('Loading issues'), + loadingMoreissues: __('Loading more issues'), + showingAllIssues: __('Showing all issues'), + }, components: { - boardCard, - boardNewIssue, + BoardCard, + BoardNewIssue, GlLoadingIcon, }, props: { @@ -37,55 +34,67 @@ export default { type: Array, required: true, }, + canAdminList: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { scrollOffset: 250, - filters: boardsStore.state.filters, showCount: false, showIssueForm: false, }; }, computed: { + ...mapState(['pageInfoByListId', 'listsFlags']), paginatedIssueText() { return sprintf(__('Showing %{pageSize} of %{total} issues'), { - pageSize: this.list.issues.length, - total: this.list.issuesSize, + pageSize: this.issues.length, + total: this.list.issuesCount, }); }, issuesSizeExceedsMax() { - return this.list.maxIssueCount > 0 && this.list.issuesSize > this.list.maxIssueCount; + return this.list.maxIssueCount > 0 && this.list.issuesCount > this.list.maxIssueCount; + }, + hasNextPage() { + return this.pageInfoByListId[this.list.id].hasNextPage; }, loading() { - return this.list.loading; + return this.listsFlags[this.list.id]?.isLoading; + }, + loadingMore() { + return this.listsFlags[this.list.id]?.isLoadingMore; + }, + listRef() { + // When list is draggable, the reference to the list needs to be accessed differently + return this.canAdminList ? this.$refs.list.$el : this.$refs.list; + }, + showingAllIssues() { + return this.issues.length === this.list.issuesCount; + }, + treeRootWrapper() { + return this.canAdminList ? Draggable : 'ul'; + }, + treeRootOptions() { + const options = { + ...defaultSortableConfig, + fallbackOnBody: false, + group: 'board-list', + tag: 'ul', + 'ghost-class': 'board-card-drag-active', + 'data-list-id': this.list.id, + value: this.issues, + }; + + return this.canAdminList ? options : {}; }, }, watch: { - filters: { - handler() { - this.list.loadingMore = false; - this.$refs.list.scrollTop = 0; - }, - deep: true, - }, issues() { this.$nextTick(() => { - if ( - this.scrollHeight() <= this.listHeight() && - this.list.issuesSize > this.list.issues.length && - this.list.isExpanded - ) { - this.list.page += 1; - this.list.getIssues(false).catch(() => { - // TODO: handle request error - }); - } - - if (this.scrollHeight() > Math.ceil(this.listHeight())) { - this.showCount = true; - } else { - this.showCount = false; - } + this.showCount = this.scrollHeight() > Math.ceil(this.listHeight()); }); }, }, @@ -94,315 +103,90 @@ export default { eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop); }, mounted() { - // TODO: Use Draggable in ./board_list_new.vue to drag & drop issue - // https://gitlab.com/gitlab-org/gitlab/-/issues/218164 - const multiSelectOpts = { - multiDrag: true, - selectedClass: 'js-multi-select', - animation: 500, - }; - - const options = getBoardSortableDefaultOptions({ - scroll: true, - disabled: this.disabled, - filter: '.board-list-count, .is-disabled', - dataIdAttr: 'data-issue-id', - removeCloneOnHide: false, - ...multiSelectOpts, - group: { - name: 'issues', - /** - * Dynamically determine between which containers - * items can be moved or copied as - * Assignee lists (EE feature) require this behavior - */ - pull: (to, from, dragEl, e) => { - // As per Sortable's docs, `to` should provide - // reference to exact sortable container on which - // we're trying to drag element, but either it is - // a library's bug or our markup structure is too complex - // that `to` never points to correct container - // See https://github.com/RubaXa/Sortable/issues/1037 - // - // So we use `e.target` which is always accurate about - // which element we're currently dragging our card upon - // So from there, we can get reference to actual container - // and thus the container type to enable Copy or Move - if (e.target) { - const containerEl = - e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list'); - const toBoardType = containerEl.dataset.boardType; - const cloneActions = { - label: ['milestone', 'assignee'], - assignee: ['milestone', 'label'], - milestone: ['label', 'assignee'], - }; - - if (toBoardType) { - const fromBoardType = this.list.type; - // For each list we check if the destination list is - // a the list were we should clone the issue - const shouldClone = Object.entries(cloneActions).some( - entry => fromBoardType === entry[0] && entry[1].includes(toBoardType), - ); - - if (shouldClone) { - return 'clone'; - } - } - } - - return true; - }, - revertClone: true, - }, - onStart: e => { - const card = this.$refs.issue[e.oldIndex]; - - card.showDetail = false; - - const { list } = card; - - const issue = list.findIssue(Number(e.item.dataset.issueId)); - - boardsStore.startMoving(list, issue); - - this.$root.$emit('bv::hide::tooltip'); - - sortableStart(); - }, - onAdd: e => { - const { items = [], newIndicies = [] } = e; - if (items.length) { - // Not using e.newIndex here instead taking a min of all - // the newIndicies. Basically we have to find that during - // a drop what is the index we're going to start putting - // all the dropped elements from. - const newIndex = Math.min(...newIndicies.map(obj => obj.index).filter(i => i !== -1)); - const issues = items.map(item => - boardsStore.moving.list.findIssue(Number(item.dataset.issueId)), - ); - - boardsStore.moveMultipleIssuesToList({ - listFrom: boardsStore.moving.list, - listTo: this.list, - issues, - newIndex, - }); - } else { - boardsStore.moveIssueToList( - boardsStore.moving.list, - this.list, - boardsStore.moving.issue, - e.newIndex, - ); - this.$nextTick(() => { - e.item.remove(); - }); - } - }, - onUpdate: e => { - const sortedArray = this.sortable.toArray().filter(id => id !== '-1'); - - const { items = [], newIndicies = [], oldIndicies = [] } = e; - if (items.length) { - const newIndex = Math.min(...newIndicies.map(obj => obj.index)); - const issues = items.map(item => - boardsStore.moving.list.findIssue(Number(item.dataset.issueId)), - ); - boardsStore.moveMultipleIssuesInList({ - list: this.list, - issues, - oldIndicies: oldIndicies.map(obj => obj.index), - newIndex, - idArray: sortedArray, - }); - e.items.forEach(el => { - Sortable.utils.deselect(el); - }); - boardsStore.clearMultiSelect(); - return; - } - - boardsStore.moveIssueInList( - this.list, - boardsStore.moving.issue, - e.oldIndex, - e.newIndex, - sortedArray, - ); - }, - onEnd: e => { - const { items = [], clones = [], to } = e; - - // This is not a multi select operation - if (!items.length && !clones.length) { - sortableEnd(); - return; - } - - let toList; - if (to) { - const containerEl = to.closest('.js-board-list'); - toList = boardsStore.findList('id', Number(containerEl.dataset.board), ''); - } - - /** - * onEnd is called irrespective if the cards were moved in the - * same list or the other list. Don't remove items if it's same list. - */ - const isSameList = toList && toList.id === this.list.id; - if (toList && !isSameList && boardsStore.shouldRemoveIssue(this.list, toList)) { - const issues = items.map(item => this.list.findIssue(Number(item.dataset.issueId))); - if ( - issues.filter(Boolean).length && - !boardsStore.issuesAreContiguous(this.list, issues) - ) { - const indexes = []; - const ids = this.list.issues.map(i => i.id); - issues.forEach(issue => { - const index = ids.indexOf(issue.id); - if (index > -1) { - indexes.push(index); - } - }); - - // Descending sort because splice would cause index discrepancy otherwise - const sortedIndexes = indexes.sort((a, b) => (a < b ? 1 : -1)); - - sortedIndexes.forEach(i => { - /** - * **setTimeout and splice each element one-by-one in a loop - * is intended.** - * - * The problem here is all the indexes are in the list but are - * non-contiguous. Due to that, when we splice all the indexes, - * at once, Vue -- during a re-render -- is unable to find reference - * nodes and the entire app crashes. - * - * If the indexes are contiguous, this piece of code is not - * executed. If it is, this is a possible regression. Only when - * issue indexes are far apart, this logic should ever kick in. - */ - setTimeout(() => { - this.list.issues.splice(i, 1); - }, 0); - }); - } - } - - if (!toList) { - createFlash(__('Something went wrong while performing the action.')); - } - - if (!isSameList) { - boardsStore.clearMultiSelect(); - - // Since Vue's list does not re-render the same keyed item, we'll - // remove `multi-select` class to express it's unselected - if (clones && clones.length) { - clones.forEach(el => el.classList.remove('multi-select')); - } - - // Due to some bug which I am unable to figure out - // Sortable does not deselect some pending items from the - // source list. - // We'll just do it forcefully here. - Array.from(document.querySelectorAll('.js-multi-select') || []).forEach(item => { - Sortable.utils.deselect(item); - }); - - /** - * SortableJS leaves all the moving items "as is" on the DOM. - * Vue picks up and rehydrates the DOM, but we need to explicity - * remove the "trash" items from the DOM. - * - * This is in parity to the logic on single item move from a list/in - * a list. For reference, look at the implementation of onAdd method. - */ - this.$nextTick(() => { - if (items && items.length) { - items.forEach(item => { - item.remove(); - }); - } - }); - } - sortableEnd(); - }, - onMove(e) { - return !e.related.classList.contains('board-list-count'); - }, - onSelect(e) { - const { - item: { classList }, - } = e; - - if ( - classList && - classList.contains('js-multi-select') && - !classList.contains('multi-select') - ) { - Sortable.utils.deselect(e.item); - } - }, - onDeselect: e => { - const { - item: { dataset, classList }, - } = e; - - if ( - classList && - classList.contains('multi-select') && - !classList.contains('js-multi-select') - ) { - const issue = this.list.findIssue(Number(dataset.issueId)); - boardsStore.toggleMultiSelect(issue); - } - }, - }); - - this.sortable = Sortable.create(this.$refs.list, options); - // Scroll event on list to load more - this.$refs.list.addEventListener('scroll', this.onScroll); + this.listRef.addEventListener('scroll', this.onScroll); }, beforeDestroy() { eventHub.$off(`toggle-issue-form-${this.list.id}`, this.toggleForm); eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop); - this.$refs.list.removeEventListener('scroll', this.onScroll); + this.listRef.removeEventListener('scroll', this.onScroll); }, methods: { + ...mapActions(['fetchIssuesForList', 'moveIssue']), listHeight() { - return this.$refs.list.getBoundingClientRect().height; + return this.listRef.getBoundingClientRect().height; }, scrollHeight() { - return this.$refs.list.scrollHeight; + return this.listRef.scrollHeight; }, scrollTop() { - return this.$refs.list.scrollTop + this.listHeight(); + return this.listRef.scrollTop + this.listHeight(); }, scrollToTop() { - this.$refs.list.scrollTop = 0; + this.listRef.scrollTop = 0; }, loadNextPage() { - const getIssues = this.list.nextPage(); - const loadingDone = () => { - this.list.loadingMore = false; - }; - - if (getIssues) { - this.list.loadingMore = true; - getIssues.then(loadingDone).catch(loadingDone); - } + this.fetchIssuesForList({ listId: this.list.id, fetchNext: true }); }, toggleForm() { this.showIssueForm = !this.showIssueForm; }, onScroll() { - if (!this.list.loadingMore && this.scrollTop() > this.scrollHeight() - this.scrollOffset) { - this.loadNextPage(); + window.requestAnimationFrame(() => { + if ( + !this.loadingMore && + this.scrollTop() > this.scrollHeight() - this.scrollOffset && + this.hasNextPage + ) { + this.loadNextPage(); + } + }); + }, + handleDragOnStart() { + sortableStart(); + }, + handleDragOnEnd(params) { + sortableEnd(); + const { newIndex, oldIndex, from, to, item } = params; + const { issueId, issueIid, issuePath } = item.dataset; + const { children } = to; + let moveBeforeId; + let moveAfterId; + + const getIssueId = (el) => Number(el.dataset.issueId); + + // If issue is being moved within the same list + if (from === to) { + if (newIndex > oldIndex && children.length > 1) { + // If issue is being moved down we look for the issue that ends up before + moveBeforeId = getIssueId(children[newIndex]); + } else if (newIndex < oldIndex && children.length > 1) { + // If issue is being moved up we look for the issue that ends up after + moveAfterId = getIssueId(children[newIndex]); + } else { + // If issue remains in the same list at the same position we do nothing + return; + } + } else { + // We look for the issue that ends up before the moved issue if it exists + if (children[newIndex - 1]) { + moveBeforeId = getIssueId(children[newIndex - 1]); + } + // We look for the issue that ends up after the moved issue if it exists + if (children[newIndex]) { + moveAfterId = getIssueId(children[newIndex]); + } } + + this.moveIssue({ + issueId, + issueIid, + issuePath, + fromListId: from.dataset.listId, + toListId: to.dataset.listId, + moveBeforeId, + moveAfterId, + }); }, }, }; @@ -410,21 +194,31 @@ export default { <template> <div - :class="{ 'd-none': !list.isExpanded, 'd-flex flex-column': list.isExpanded }" - class="board-list-component position-relative h-100" + v-show="!list.collapsed" + class="board-list-component gl-relative gl-h-full gl-display-flex gl-flex-direction-column" data-qa-selector="board_list_cards_area" > - <div v-if="loading" class="board-list-loading text-center" :aria-label="__('Loading issues')"> + <div + v-if="loading" + class="gl-mt-4 gl-text-center" + :aria-label="$options.i18n.loadingIssues" + data-testid="board_list_loading" + > <gl-loading-icon /> </div> - <board-new-issue v-if="list.type !== 'closed' && showIssueForm" :list="list" /> - <ul + <board-new-issue v-if="list.listType !== 'closed' && showIssueForm" :list="list" /> + <component + :is="treeRootWrapper" v-show="!loading" ref="list" + v-bind="treeRootOptions" :data-board="list.id" - :data-board-type="list.type" - :class="{ 'is-smaller': showIssueForm, 'bg-danger-100': issuesSizeExceedsMax }" - class="board-list w-100 h-100 list-unstyled mb-0 p-1 js-board-list" + :data-board-type="list.listType" + :class="{ 'bg-danger-100': issuesSizeExceedsMax }" + class="board-list gl-w-full gl-h-full gl-list-style-none gl-mb-0 gl-p-2 js-board-list" + data-testid="tree-root-wrapper" + @start="handleDragOnStart" + @end="handleDragOnEnd" > <board-card v-for="(issue, index) in issues" @@ -435,11 +229,11 @@ export default { :issue="issue" :disabled="disabled" /> - <li v-if="showCount" class="board-list-count text-center" data-issue-id="-1"> - <gl-loading-icon v-show="list.loadingMore" label="Loading more issues" /> - <span v-if="list.issues.length === list.issuesSize">{{ __('Showing all issues') }}</span> + <li v-if="showCount" class="board-list-count gl-text-center" data-issue-id="-1"> + <gl-loading-icon v-if="loadingMore" :label="$options.i18n.loadingMoreissues" /> + <span v-if="showingAllIssues">{{ $options.i18n.showingAllIssues }}</span> <span v-else>{{ paginatedIssueText }}</span> </li> - </ul> + </component> </div> </template> diff --git a/app/assets/javascripts/boards/components/board_list_deprecated.vue b/app/assets/javascripts/boards/components/board_list_deprecated.vue new file mode 100644 index 00000000000..24900346bda --- /dev/null +++ b/app/assets/javascripts/boards/components/board_list_deprecated.vue @@ -0,0 +1,443 @@ +<script> +import { Sortable, MultiDrag } from 'sortablejs'; +import { GlLoadingIcon } from '@gitlab/ui'; +import boardNewIssue from './board_new_issue_deprecated.vue'; +import boardCard from './board_card.vue'; +import eventHub from '../eventhub'; +import boardsStore from '../stores/boards_store'; +import { sprintf, __ } from '~/locale'; +import { deprecatedCreateFlash as createFlash } from '~/flash'; +import { + getBoardSortableDefaultOptions, + sortableStart, + sortableEnd, +} from '../mixins/sortable_default_options'; + +// This component is being replaced in favor of './board_list.vue' for GraphQL boards + +Sortable.mount(new MultiDrag()); + +export default { + name: 'BoardList', + components: { + boardCard, + boardNewIssue, + GlLoadingIcon, + }, + props: { + disabled: { + type: Boolean, + required: true, + }, + list: { + type: Object, + required: true, + }, + issues: { + type: Array, + required: true, + }, + }, + data() { + return { + scrollOffset: 250, + filters: boardsStore.state.filters, + showCount: false, + showIssueForm: false, + }; + }, + computed: { + paginatedIssueText() { + return sprintf(__('Showing %{pageSize} of %{total} issues'), { + pageSize: this.list.issues.length, + total: this.list.issuesSize, + }); + }, + issuesSizeExceedsMax() { + return this.list.maxIssueCount > 0 && this.list.issuesSize > this.list.maxIssueCount; + }, + loading() { + return this.list.loading; + }, + }, + watch: { + filters: { + handler() { + this.list.loadingMore = false; + this.$refs.list.scrollTop = 0; + }, + deep: true, + }, + issues() { + this.$nextTick(() => { + if ( + this.scrollHeight() <= this.listHeight() && + this.list.issuesSize > this.list.issues.length && + this.list.isExpanded + ) { + this.list.page += 1; + this.list.getIssues(false).catch(() => { + // TODO: handle request error + }); + } + + if (this.scrollHeight() > Math.ceil(this.listHeight())) { + this.showCount = true; + } else { + this.showCount = false; + } + }); + }, + }, + created() { + eventHub.$on(`toggle-issue-form-${this.list.id}`, this.toggleForm); + eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop); + }, + mounted() { + const multiSelectOpts = { + multiDrag: true, + selectedClass: 'js-multi-select', + animation: 500, + }; + + const options = getBoardSortableDefaultOptions({ + scroll: true, + disabled: this.disabled, + filter: '.board-list-count, .is-disabled', + dataIdAttr: 'data-issue-id', + removeCloneOnHide: false, + ...multiSelectOpts, + group: { + name: 'issues', + /** + * Dynamically determine between which containers + * items can be moved or copied as + * Assignee lists (EE feature) require this behavior + */ + pull: (to, from, dragEl, e) => { + // As per Sortable's docs, `to` should provide + // reference to exact sortable container on which + // we're trying to drag element, but either it is + // a library's bug or our markup structure is too complex + // that `to` never points to correct container + // See https://github.com/RubaXa/Sortable/issues/1037 + // + // So we use `e.target` which is always accurate about + // which element we're currently dragging our card upon + // So from there, we can get reference to actual container + // and thus the container type to enable Copy or Move + if (e.target) { + const containerEl = + e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list'); + const toBoardType = containerEl.dataset.boardType; + const cloneActions = { + label: ['milestone', 'assignee'], + assignee: ['milestone', 'label'], + milestone: ['label', 'assignee'], + }; + + if (toBoardType) { + const fromBoardType = this.list.type; + // For each list we check if the destination list is + // a the list were we should clone the issue + const shouldClone = Object.entries(cloneActions).some( + (entry) => fromBoardType === entry[0] && entry[1].includes(toBoardType), + ); + + if (shouldClone) { + return 'clone'; + } + } + } + + return true; + }, + revertClone: true, + }, + onStart: (e) => { + const card = this.$refs.issue[e.oldIndex]; + + card.showDetail = false; + + const { list } = card; + + const issue = list.findIssue(Number(e.item.dataset.issueId)); + + boardsStore.startMoving(list, issue); + + this.$root.$emit('bv::hide::tooltip'); + + sortableStart(); + }, + onAdd: (e) => { + const { items = [], newIndicies = [] } = e; + if (items.length) { + // Not using e.newIndex here instead taking a min of all + // the newIndicies. Basically we have to find that during + // a drop what is the index we're going to start putting + // all the dropped elements from. + const newIndex = Math.min(...newIndicies.map((obj) => obj.index).filter((i) => i !== -1)); + const issues = items.map((item) => + boardsStore.moving.list.findIssue(Number(item.dataset.issueId)), + ); + + boardsStore.moveMultipleIssuesToList({ + listFrom: boardsStore.moving.list, + listTo: this.list, + issues, + newIndex, + }); + } else { + boardsStore.moveIssueToList( + boardsStore.moving.list, + this.list, + boardsStore.moving.issue, + e.newIndex, + ); + this.$nextTick(() => { + e.item.remove(); + }); + } + }, + onUpdate: (e) => { + const sortedArray = this.sortable.toArray().filter((id) => id !== '-1'); + + const { items = [], newIndicies = [], oldIndicies = [] } = e; + if (items.length) { + const newIndex = Math.min(...newIndicies.map((obj) => obj.index)); + const issues = items.map((item) => + boardsStore.moving.list.findIssue(Number(item.dataset.issueId)), + ); + boardsStore.moveMultipleIssuesInList({ + list: this.list, + issues, + oldIndicies: oldIndicies.map((obj) => obj.index), + newIndex, + idArray: sortedArray, + }); + e.items.forEach((el) => { + Sortable.utils.deselect(el); + }); + boardsStore.clearMultiSelect(); + return; + } + + boardsStore.moveIssueInList( + this.list, + boardsStore.moving.issue, + e.oldIndex, + e.newIndex, + sortedArray, + ); + }, + onEnd: (e) => { + const { items = [], clones = [], to } = e; + + // This is not a multi select operation + if (!items.length && !clones.length) { + sortableEnd(); + return; + } + + let toList; + if (to) { + const containerEl = to.closest('.js-board-list'); + toList = boardsStore.findList('id', Number(containerEl.dataset.board)); + } + + /** + * onEnd is called irrespective if the cards were moved in the + * same list or the other list. Don't remove items if it's same list. + */ + const isSameList = toList && toList.id === this.list.id; + if (toList && !isSameList && boardsStore.shouldRemoveIssue(this.list, toList)) { + const issues = items.map((item) => this.list.findIssue(Number(item.dataset.issueId))); + if ( + issues.filter(Boolean).length && + !boardsStore.issuesAreContiguous(this.list, issues) + ) { + const indexes = []; + const ids = this.list.issues.map((i) => i.id); + issues.forEach((issue) => { + const index = ids.indexOf(issue.id); + if (index > -1) { + indexes.push(index); + } + }); + + // Descending sort because splice would cause index discrepancy otherwise + const sortedIndexes = indexes.sort((a, b) => (a < b ? 1 : -1)); + + sortedIndexes.forEach((i) => { + /** + * **setTimeout and splice each element one-by-one in a loop + * is intended.** + * + * The problem here is all the indexes are in the list but are + * non-contiguous. Due to that, when we splice all the indexes, + * at once, Vue -- during a re-render -- is unable to find reference + * nodes and the entire app crashes. + * + * If the indexes are contiguous, this piece of code is not + * executed. If it is, this is a possible regression. Only when + * issue indexes are far apart, this logic should ever kick in. + */ + setTimeout(() => { + this.list.issues.splice(i, 1); + }, 0); + }); + } + } + + if (!toList) { + createFlash(__('Something went wrong while performing the action.')); + } + + if (!isSameList) { + boardsStore.clearMultiSelect(); + + // Since Vue's list does not re-render the same keyed item, we'll + // remove `multi-select` class to express it's unselected + if (clones && clones.length) { + clones.forEach((el) => el.classList.remove('multi-select')); + } + + // Due to some bug which I am unable to figure out + // Sortable does not deselect some pending items from the + // source list. + // We'll just do it forcefully here. + Array.from(document.querySelectorAll('.js-multi-select') || []).forEach((item) => { + Sortable.utils.deselect(item); + }); + + /** + * SortableJS leaves all the moving items "as is" on the DOM. + * Vue picks up and rehydrates the DOM, but we need to explicity + * remove the "trash" items from the DOM. + * + * This is in parity to the logic on single item move from a list/in + * a list. For reference, look at the implementation of onAdd method. + */ + this.$nextTick(() => { + if (items && items.length) { + items.forEach((item) => { + item.remove(); + }); + } + }); + } + sortableEnd(); + }, + onMove(e) { + return !e.related.classList.contains('board-list-count'); + }, + onSelect(e) { + const { + item: { classList }, + } = e; + + if ( + classList && + classList.contains('js-multi-select') && + !classList.contains('multi-select') + ) { + Sortable.utils.deselect(e.item); + } + }, + onDeselect: (e) => { + const { + item: { dataset, classList }, + } = e; + + if ( + classList && + classList.contains('multi-select') && + !classList.contains('js-multi-select') + ) { + const issue = this.list.findIssue(Number(dataset.issueId)); + boardsStore.toggleMultiSelect(issue); + } + }, + }); + + this.sortable = Sortable.create(this.$refs.list, options); + + // Scroll event on list to load more + this.$refs.list.addEventListener('scroll', this.onScroll); + }, + beforeDestroy() { + eventHub.$off(`toggle-issue-form-${this.list.id}`, this.toggleForm); + eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop); + this.$refs.list.removeEventListener('scroll', this.onScroll); + }, + methods: { + listHeight() { + return this.$refs.list.getBoundingClientRect().height; + }, + scrollHeight() { + return this.$refs.list.scrollHeight; + }, + scrollTop() { + return this.$refs.list.scrollTop + this.listHeight(); + }, + scrollToTop() { + this.$refs.list.scrollTop = 0; + }, + loadNextPage() { + const getIssues = this.list.nextPage(); + const loadingDone = () => { + this.list.loadingMore = false; + }; + + if (getIssues) { + this.list.loadingMore = true; + getIssues.then(loadingDone).catch(loadingDone); + } + }, + toggleForm() { + this.showIssueForm = !this.showIssueForm; + }, + onScroll() { + if (!this.list.loadingMore && this.scrollTop() > this.scrollHeight() - this.scrollOffset) { + this.loadNextPage(); + } + }, + }, +}; +</script> + +<template> + <div + :class="{ 'd-none': !list.isExpanded, 'd-flex flex-column': list.isExpanded }" + class="board-list-component position-relative h-100" + data-qa-selector="board_list_cards_area" + > + <div v-if="loading" class="board-list-loading text-center" :aria-label="__('Loading issues')"> + <gl-loading-icon /> + </div> + <board-new-issue v-if="list.type !== 'closed' && showIssueForm" :list="list" /> + <ul + v-show="!loading" + ref="list" + :data-board="list.id" + :data-board-type="list.type" + :class="{ 'is-smaller': showIssueForm, 'bg-danger-100': issuesSizeExceedsMax }" + class="board-list w-100 h-100 list-unstyled mb-0 p-1 js-board-list" + > + <board-card + v-for="(issue, index) in issues" + ref="issue" + :key="issue.id" + :index="index" + :list="list" + :issue="issue" + :disabled="disabled" + /> + <li v-if="showCount" class="board-list-count text-center" data-issue-id="-1"> + <gl-loading-icon v-show="list.loadingMore" label="Loading more issues" /> + <span v-if="list.issues.length === list.issuesSize">{{ __('Showing all issues') }}</span> + <span v-else>{{ paginatedIssueText }}</span> + </li> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue index 3db5c2e0830..06f39eceb08 100644 --- a/app/assets/javascripts/boards/components/board_list_header.vue +++ b/app/assets/javascripts/boards/components/board_list_header.vue @@ -9,16 +9,22 @@ import { GlSprintf, GlTooltipDirective, } from '@gitlab/ui'; -import { n__, s__ } from '~/locale'; +import { n__, s__, __ } from '~/locale'; import AccessorUtilities from '../../lib/utils/accessor'; import IssueCount from './issue_count.vue'; -import boardsStore from '../stores/boards_store'; import eventHub from '../eventhub'; import sidebarEventHub from '~/sidebar/event_hub'; import { inactiveId, LIST, ListType } from '../constants'; import { isScopedLabel } from '~/lib/utils/common_utils'; +import { isListDraggable } from '~/boards/boards_util'; export default { + i18n: { + newIssue: __('New issue'), + listSettings: __('List settings'), + expand: s__('Boards|Expand'), + collapse: s__('Boards|Collapse'), + }, components: { GlButtonGroup, GlButton, @@ -31,6 +37,20 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + inject: { + boardId: { + default: '', + }, + weightFeatureAvailable: { + default: false, + }, + scopedLabelsAvailable: { + default: false, + }, + currentUserId: { + default: null, + }, + }, props: { list: { type: Object, @@ -47,61 +67,53 @@ export default { default: false, }, }, - inject: { - boardId: { - default: '', - }, - }, - data() { - return { - weightFeatureAvailable: false, - }; - }, computed: { ...mapState(['activeId']), isLoggedIn() { - return Boolean(gon.current_user_id); + return Boolean(this.currentUserId); }, listType() { - return this.list.type; + return this.list.listType; }, listAssignee() { return this.list?.assignee?.username || ''; }, listTitle() { - return this.list?.label?.description || this.list.title || ''; + return this.list?.label?.description || this.list?.assignee?.name || this.list.title || ''; }, showListHeaderButton() { return !this.disabled && this.listType !== ListType.closed; }, showMilestoneListDetails() { return ( - this.list.type === 'milestone' && + this.listType === ListType.milestone && this.list.milestone && - (this.list.isExpanded || !this.isSwimlanesHeader) + (!this.list.collapsed || !this.isSwimlanesHeader) ); }, showAssigneeListDetails() { - return this.list.type === 'assignee' && (this.list.isExpanded || !this.isSwimlanesHeader); + return ( + this.listType === ListType.assignee && (!this.list.collapsed || !this.isSwimlanesHeader) + ); }, issuesCount() { - return this.list.issuesSize; + return this.list.issuesCount; }, issuesTooltipLabel() { return n__(`%d issue`, `%d issues`, this.issuesCount); }, chevronTooltip() { - return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand'); + return this.list.collapsed ? this.$options.i18n.expand : this.$options.i18n.collapse; }, chevronIcon() { - return this.list.isExpanded ? 'chevron-right' : 'chevron-down'; + return this.list.collapsed ? 'chevron-down' : 'chevron-right'; }, isNewIssueShown() { return this.listType === ListType.backlog || this.showListHeaderButton; }, isSettingsShown() { return ( - this.listType !== ListType.backlog && this.showListHeaderButton && this.list.isExpanded + this.listType !== ListType.backlog && this.showListHeaderButton && !this.list.collapsed ); }, uniqueKey() { @@ -111,9 +123,15 @@ export default { collapsedTooltipTitle() { return this.listTitle || this.listAssignee; }, + headerStyle() { + return { borderTopColor: this.list?.label?.color }; + }, + userCanDrag() { + return !this.disabled && isListDraggable(this.list); + }, }, methods: { - ...mapActions(['setActiveId']), + ...mapActions(['updateList', 'setActiveId']), openSidebarSettings() { if (this.activeId === inactiveId) { sidebarEventHub.$emit('sidebar.closeAll'); @@ -122,14 +140,14 @@ export default { this.setActiveId({ id: this.list.id, sidebarType: LIST }); }, showScopedLabels(label) { - return boardsStore.scopedLabels.enabled && isScopedLabel(label); + return this.scopedLabelsAvailable && isScopedLabel(label); }, showNewIssueForm() { eventHub.$emit(`toggle-issue-form-${this.list.id}`); }, toggleExpanded() { - this.list.isExpanded = !this.list.isExpanded; + this.list.collapsed = !this.list.collapsed; if (!this.isLoggedIn) { this.addToLocalStorage(); @@ -143,11 +161,11 @@ export default { }, addToLocalStorage() { if (AccessorUtilities.isLocalStorageAccessSafe()) { - localStorage.setItem(`${this.uniqueKey}.expanded`, this.list.isExpanded); + localStorage.setItem(`${this.uniqueKey}.expanded`, !this.list.collapsed); } }, updateListFunction() { - this.list.update(); + this.updateList({ listId: this.list.id, collapsed: this.list.collapsed }); }, }, }; @@ -157,26 +175,25 @@ export default { <header :class="{ 'has-border': list.label && list.label.color, - 'gl-h-full': !list.isExpanded, + 'gl-h-full': list.collapsed, 'board-inner gl-rounded-top-left-base gl-rounded-top-right-base': isSwimlanesHeader, }" - :style="{ borderTopColor: list.label && list.label.color ? list.label.color : null }" + :style="headerStyle" class="board-header gl-relative" data-qa-selector="board_list_header" data-testid="board-list-header" > <h3 :class="{ - 'user-can-drag': !disabled && !list.preset, - 'gl-py-3 gl-h-full': !list.isExpanded && !isSwimlanesHeader, - 'gl-border-b-0': !list.isExpanded || isSwimlanesHeader, - 'gl-py-2': !list.isExpanded && isSwimlanesHeader, - 'gl-flex-direction-column': !list.isExpanded, + 'user-can-drag': userCanDrag, + 'gl-py-3 gl-h-full': list.collapsed && !isSwimlanesHeader, + 'gl-border-b-0': list.collapsed || isSwimlanesHeader, + 'gl-py-2': list.collapsed && isSwimlanesHeader, + 'gl-flex-direction-column': list.collapsed, }" class="board-title gl-m-0 gl-display-flex gl-align-items-center gl-font-base gl-px-3 js-board-handle" > <gl-button - v-if="list.isExpandable" v-gl-tooltip.hover :aria-label="chevronTooltip" :title="chevronTooltip" @@ -186,14 +203,14 @@ export default { size="small" @click="toggleExpanded" /> - <!-- The following is only true in EE and if it is a milestone --> + <!-- EE start --> <span v-if="showMilestoneListDetails" aria-hidden="true" class="milestone-icon" :class="{ - 'gl-mt-3 gl-rotate-90': !list.isExpanded, - 'gl-mr-2': list.isExpanded, + 'gl-mt-3 gl-rotate-90': list.collapsed, + 'gl-mr-2': !list.collapsed, }" > <gl-icon name="timer" /> @@ -201,90 +218,95 @@ export default { <a v-if="showAssigneeListDetails" - :href="list.assignee.path" + :href="list.assignee.webUrl" class="user-avatar-link js-no-trigger" :class="{ - 'gl-mt-3 gl-rotate-90': !list.isExpanded, + 'gl-mt-3 gl-rotate-90': list.collapsed, }" > <img v-gl-tooltip.hover.bottom :title="listAssignee" :alt="list.assignee.name" - :src="list.assignee.avatar" + :src="list.assignee.avatarUrl" class="avatar s20" height="20" width="20" /> </a> + <!-- EE end --> <div class="board-title-text" :class="{ - 'gl-display-none': !list.isExpanded && isSwimlanesHeader, - 'gl-flex-grow-0 gl-my-3 gl-mx-0': !list.isExpanded, - 'gl-flex-grow-1': list.isExpanded, + 'gl-display-none': list.collapsed && isSwimlanesHeader, + 'gl-flex-grow-0 gl-my-3 gl-mx-0': list.collapsed, + 'gl-flex-grow-1': !list.collapsed, }" > + <!-- EE start --> <span - v-if="list.type !== 'label'" + v-if="listType !== 'label'" v-gl-tooltip.hover :class="{ - 'gl-display-block': !list.isExpanded || list.type === 'milestone', + 'gl-display-block': list.collapsed || listType === 'milestone', }" :title="listTitle" class="board-title-main-text gl-text-truncate" > - {{ list.title }} + {{ listTitle }} </span> <span - v-if="list.type === 'assignee'" + v-if="listType === 'assignee'" + v-show="!list.collapsed" class="gl-ml-2 gl-font-weight-normal gl-text-gray-500" - :class="{ 'gl-display-none': !list.isExpanded }" > @{{ listAssignee }} </span> + <!-- EE end --> <gl-label - v-if="list.type === 'label'" + v-if="listType === 'label'" v-gl-tooltip.hover.bottom :background-color="list.label.color" :description="list.label.description" :scoped="showScopedLabels(list.label)" - :size="!list.isExpanded ? 'sm' : ''" + :size="list.collapsed ? 'sm' : ''" :title="list.label.title" /> </div> + <!-- EE start --> <span - v-if="isSwimlanesHeader && !list.isExpanded" + v-if="isSwimlanesHeader && list.collapsed" ref="collapsedInfo" aria-hidden="true" - class="board-header-collapsed-info-icon gl-mt-2 gl-cursor-pointer gl-text-gray-500" + class="board-header-collapsed-info-icon gl-cursor-pointer gl-text-gray-500" > <gl-icon name="information" /> </span> - <gl-tooltip v-if="isSwimlanesHeader && !list.isExpanded" :target="() => $refs.collapsedInfo"> + <gl-tooltip v-if="isSwimlanesHeader && list.collapsed" :target="() => $refs.collapsedInfo"> <div class="gl-font-weight-bold gl-pb-2">{{ collapsedTooltipTitle }}</div> <div v-if="list.maxIssueCount !== 0"> - • + ā¢ <gl-sprintf :message="__('%{issuesSize} with a limit of %{maxIssueCount}')"> <template #issuesSize>{{ issuesTooltipLabel }}</template> <template #maxIssueCount>{{ list.maxIssueCount }}</template> </gl-sprintf> </div> - <div v-else>• {{ issuesTooltipLabel }}</div> + <div v-else>ā¢ {{ issuesTooltipLabel }}</div> <div v-if="weightFeatureAvailable"> - • + ā¢ <gl-sprintf :message="__('%{totalWeight} total weight')"> <template #totalWeight>{{ list.totalWeight }}</template> </gl-sprintf> </div> </gl-tooltip> + <!-- EE end --> <div - class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag text-secondary" + class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag gl-text-gray-500" :class="{ - 'gl-display-none!': !list.isExpanded && isSwimlanesHeader, - 'gl-p-0': !list.isExpanded, + 'gl-display-none!': list.collapsed && isSwimlanesHeader, + 'gl-p-0': list.collapsed, }" > <span class="gl-display-inline-flex"> @@ -293,7 +315,7 @@ export default { <gl-icon class="gl-mr-2" name="issues" /> <issue-count :issues-size="issuesCount" :max-issue-count="list.maxIssueCount" /> </span> - <!-- The following is only true in EE. --> + <!-- EE start --> <template v-if="weightFeatureAvailable"> <gl-tooltip :target="() => $refs.weightTooltip" :title="weightCountToolTip" /> <span ref="weightTooltip" class="gl-display-inline-flex gl-ml-3"> @@ -301,6 +323,7 @@ export default { {{ list.totalWeight }} </span> </template> + <!-- EE end --> </span> </div> <gl-button-group @@ -309,13 +332,11 @@ export default { > <gl-button v-if="isNewIssueShown" + v-show="!list.collapsed" ref="newIssueBtn" v-gl-tooltip.hover - :class="{ - 'gl-display-none': !list.isExpanded, - }" - :aria-label="__('New issue')" - :title="__('New issue')" + :aria-label="$options.i18n.newIssue" + :title="$options.i18n.newIssue" class="issue-count-badge-add-button no-drag" icon="plus" @click="showNewIssueForm" @@ -325,13 +346,13 @@ export default { v-if="isSettingsShown" ref="settingsBtn" v-gl-tooltip.hover - :aria-label="__('List settings')" + :aria-label="$options.i18n.listSettings" class="no-drag js-board-settings-button" - :title="__('List settings')" + :title="$options.i18n.listSettings" icon="settings" @click="openSidebarSettings" /> - <gl-tooltip :target="() => $refs.settingsBtn">{{ __('List settings') }}</gl-tooltip> + <gl-tooltip :target="() => $refs.settingsBtn">{{ $options.i18n.listSettings }}</gl-tooltip> </gl-button-group> </h3> </header> diff --git a/app/assets/javascripts/boards/components/board_list_header_new.vue b/app/assets/javascripts/boards/components/board_list_header_deprecated.vue index 44eb2aa34c2..21147f1616c 100644 --- a/app/assets/javascripts/boards/components/board_list_header_new.vue +++ b/app/assets/javascripts/boards/components/board_list_header_deprecated.vue @@ -9,22 +9,18 @@ import { GlSprintf, GlTooltipDirective, } from '@gitlab/ui'; -import { n__, s__, __ } from '~/locale'; +import { n__, s__ } from '~/locale'; import AccessorUtilities from '../../lib/utils/accessor'; import IssueCount from './issue_count.vue'; +import boardsStore from '../stores/boards_store'; import eventHub from '../eventhub'; import sidebarEventHub from '~/sidebar/event_hub'; import { inactiveId, LIST, ListType } from '../constants'; import { isScopedLabel } from '~/lib/utils/common_utils'; -import { isListDraggable } from '~/boards/boards_util'; + +// This component is being replaced in favor of './board_list_header.vue' for GraphQL boards export default { - i18n: { - newIssue: __('New issue'), - listSettings: __('List settings'), - expand: s__('Boards|Expand'), - collapse: s__('Boards|Collapse'), - }, components: { GlButtonGroup, GlButton, @@ -37,6 +33,11 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + inject: { + boardId: { + default: '', + }, + }, props: { list: { type: Object, @@ -53,67 +54,56 @@ export default { default: false, }, }, - inject: { - boardId: { - default: '', - }, - weightFeatureAvailable: { - default: false, - }, - scopedLabelsAvailable: { - default: false, - }, - currentUserId: { - default: null, - }, + data() { + return { + weightFeatureAvailable: false, + }; }, computed: { ...mapState(['activeId']), isLoggedIn() { - return Boolean(this.currentUserId); + return Boolean(gon.current_user_id); }, listType() { - return this.list.listType; + return this.list.type; }, listAssignee() { return this.list?.assignee?.username || ''; }, listTitle() { - return this.list?.label?.description || this.list?.assignee?.name || this.list.title || ''; + return this.list?.label?.description || this.list.title || ''; }, showListHeaderButton() { return !this.disabled && this.listType !== ListType.closed; }, showMilestoneListDetails() { return ( - this.listType === ListType.milestone && + this.list.type === 'milestone' && this.list.milestone && - (!this.list.collapsed || !this.isSwimlanesHeader) + (this.list.isExpanded || !this.isSwimlanesHeader) ); }, showAssigneeListDetails() { - return ( - this.listType === ListType.assignee && (!this.list.collapsed || !this.isSwimlanesHeader) - ); + return this.list.type === 'assignee' && (this.list.isExpanded || !this.isSwimlanesHeader); }, issuesCount() { - return this.list.issuesCount; + return this.list.issuesSize; }, issuesTooltipLabel() { return n__(`%d issue`, `%d issues`, this.issuesCount); }, chevronTooltip() { - return this.list.collapsed ? this.$options.i18n.expand : this.$options.i18n.collapse; + return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand'); }, chevronIcon() { - return this.list.collapsed ? 'chevron-down' : 'chevron-right'; + return this.list.isExpanded ? 'chevron-right' : 'chevron-down'; }, isNewIssueShown() { return this.listType === ListType.backlog || this.showListHeaderButton; }, isSettingsShown() { return ( - this.listType !== ListType.backlog && this.showListHeaderButton && !this.list.collapsed + this.listType !== ListType.backlog && this.showListHeaderButton && this.list.isExpanded ); }, uniqueKey() { @@ -123,15 +113,9 @@ export default { collapsedTooltipTitle() { return this.listTitle || this.listAssignee; }, - headerStyle() { - return { borderTopColor: this.list?.label?.color }; - }, - userCanDrag() { - return !this.disabled && isListDraggable(this.list); - }, }, methods: { - ...mapActions(['updateList', 'setActiveId']), + ...mapActions(['setActiveId']), openSidebarSettings() { if (this.activeId === inactiveId) { sidebarEventHub.$emit('sidebar.closeAll'); @@ -140,14 +124,14 @@ export default { this.setActiveId({ id: this.list.id, sidebarType: LIST }); }, showScopedLabels(label) { - return this.scopedLabelsAvailable && isScopedLabel(label); + return boardsStore.scopedLabels.enabled && isScopedLabel(label); }, showNewIssueForm() { eventHub.$emit(`toggle-issue-form-${this.list.id}`); }, toggleExpanded() { - this.list.collapsed = !this.list.collapsed; + this.list.isExpanded = !this.list.isExpanded; if (!this.isLoggedIn) { this.addToLocalStorage(); @@ -161,11 +145,11 @@ export default { }, addToLocalStorage() { if (AccessorUtilities.isLocalStorageAccessSafe()) { - localStorage.setItem(`${this.uniqueKey}.expanded`, !this.list.collapsed); + localStorage.setItem(`${this.uniqueKey}.expanded`, this.list.isExpanded); } }, updateListFunction() { - this.updateList({ listId: this.list.id, collapsed: this.list.collapsed }); + this.list.update(); }, }, }; @@ -175,25 +159,26 @@ export default { <header :class="{ 'has-border': list.label && list.label.color, - 'gl-h-full': list.collapsed, + 'gl-h-full': !list.isExpanded, 'board-inner gl-rounded-top-left-base gl-rounded-top-right-base': isSwimlanesHeader, }" - :style="headerStyle" + :style="{ borderTopColor: list.label && list.label.color ? list.label.color : null }" class="board-header gl-relative" data-qa-selector="board_list_header" data-testid="board-list-header" > <h3 :class="{ - 'user-can-drag': userCanDrag, - 'gl-py-3 gl-h-full': list.collapsed && !isSwimlanesHeader, - 'gl-border-b-0': list.collapsed || isSwimlanesHeader, - 'gl-py-2': list.collapsed && isSwimlanesHeader, - 'gl-flex-direction-column': list.collapsed, + 'user-can-drag': !disabled && !list.preset, + 'gl-py-3 gl-h-full': !list.isExpanded && !isSwimlanesHeader, + 'gl-border-b-0': !list.isExpanded || isSwimlanesHeader, + 'gl-py-2': !list.isExpanded && isSwimlanesHeader, + 'gl-flex-direction-column': !list.isExpanded, }" class="board-title gl-m-0 gl-display-flex gl-align-items-center gl-font-base gl-px-3 js-board-handle" > <gl-button + v-if="list.isExpandable" v-gl-tooltip.hover :aria-label="chevronTooltip" :title="chevronTooltip" @@ -203,14 +188,14 @@ export default { size="small" @click="toggleExpanded" /> - <!-- EE start --> + <!-- The following is only true in EE and if it is a milestone --> <span v-if="showMilestoneListDetails" aria-hidden="true" class="milestone-icon" :class="{ - 'gl-mt-3 gl-rotate-90': list.collapsed, - 'gl-mr-2': !list.collapsed, + 'gl-mt-3 gl-rotate-90': !list.isExpanded, + 'gl-mr-2': list.isExpanded, }" > <gl-icon name="timer" /> @@ -218,95 +203,90 @@ export default { <a v-if="showAssigneeListDetails" - :href="list.assignee.webUrl" + :href="list.assignee.path" class="user-avatar-link js-no-trigger" :class="{ - 'gl-mt-3 gl-rotate-90': list.collapsed, + 'gl-mt-3 gl-rotate-90': !list.isExpanded, }" > <img v-gl-tooltip.hover.bottom :title="listAssignee" :alt="list.assignee.name" - :src="list.assignee.avatarUrl" + :src="list.assignee.avatar" class="avatar s20" height="20" width="20" /> </a> - <!-- EE end --> <div class="board-title-text" :class="{ - 'gl-display-none': list.collapsed && isSwimlanesHeader, - 'gl-flex-grow-0 gl-my-3 gl-mx-0': list.collapsed, - 'gl-flex-grow-1': !list.collapsed, + 'gl-display-none': !list.isExpanded && isSwimlanesHeader, + 'gl-flex-grow-0 gl-my-3 gl-mx-0': !list.isExpanded, + 'gl-flex-grow-1': list.isExpanded, }" > - <!-- EE start --> <span - v-if="listType !== 'label'" + v-if="list.type !== 'label'" v-gl-tooltip.hover :class="{ - 'gl-display-block': list.collapsed || listType === 'milestone', + 'gl-display-block': !list.isExpanded || list.type === 'milestone', }" :title="listTitle" class="board-title-main-text gl-text-truncate" > - {{ listTitle }} + {{ list.title }} </span> <span - v-if="listType === 'assignee'" - v-show="!list.collapsed" + v-if="list.type === 'assignee'" class="gl-ml-2 gl-font-weight-normal gl-text-gray-500" + :class="{ 'gl-display-none': !list.isExpanded }" > @{{ listAssignee }} </span> - <!-- EE end --> <gl-label - v-if="listType === 'label'" + v-if="list.type === 'label'" v-gl-tooltip.hover.bottom :background-color="list.label.color" :description="list.label.description" :scoped="showScopedLabels(list.label)" - :size="list.collapsed ? 'sm' : ''" + :size="!list.isExpanded ? 'sm' : ''" :title="list.label.title" /> </div> - <!-- EE start --> <span - v-if="isSwimlanesHeader && list.collapsed" + v-if="isSwimlanesHeader && !list.isExpanded" ref="collapsedInfo" aria-hidden="true" - class="board-header-collapsed-info-icon gl-cursor-pointer gl-text-gray-500" + class="board-header-collapsed-info-icon gl-mt-2 gl-cursor-pointer gl-text-gray-500" > <gl-icon name="information" /> </span> - <gl-tooltip v-if="isSwimlanesHeader && list.collapsed" :target="() => $refs.collapsedInfo"> + <gl-tooltip v-if="isSwimlanesHeader && !list.isExpanded" :target="() => $refs.collapsedInfo"> <div class="gl-font-weight-bold gl-pb-2">{{ collapsedTooltipTitle }}</div> <div v-if="list.maxIssueCount !== 0"> - ā¢ + • <gl-sprintf :message="__('%{issuesSize} with a limit of %{maxIssueCount}')"> <template #issuesSize>{{ issuesTooltipLabel }}</template> <template #maxIssueCount>{{ list.maxIssueCount }}</template> </gl-sprintf> </div> - <div v-else>ā¢ {{ issuesTooltipLabel }}</div> + <div v-else>• {{ issuesTooltipLabel }}</div> <div v-if="weightFeatureAvailable"> - ā¢ + • <gl-sprintf :message="__('%{totalWeight} total weight')"> <template #totalWeight>{{ list.totalWeight }}</template> </gl-sprintf> </div> </gl-tooltip> - <!-- EE end --> <div - class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag gl-text-gray-500" + class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag text-secondary" :class="{ - 'gl-display-none!': list.collapsed && isSwimlanesHeader, - 'gl-p-0': list.collapsed, + 'gl-display-none!': !list.isExpanded && isSwimlanesHeader, + 'gl-p-0': !list.isExpanded, }" > <span class="gl-display-inline-flex"> @@ -315,7 +295,7 @@ export default { <gl-icon class="gl-mr-2" name="issues" /> <issue-count :issues-size="issuesCount" :max-issue-count="list.maxIssueCount" /> </span> - <!-- EE start --> + <!-- The following is only true in EE. --> <template v-if="weightFeatureAvailable"> <gl-tooltip :target="() => $refs.weightTooltip" :title="weightCountToolTip" /> <span ref="weightTooltip" class="gl-display-inline-flex gl-ml-3"> @@ -323,7 +303,6 @@ export default { {{ list.totalWeight }} </span> </template> - <!-- EE end --> </span> </div> <gl-button-group @@ -332,11 +311,13 @@ export default { > <gl-button v-if="isNewIssueShown" - v-show="!list.collapsed" ref="newIssueBtn" v-gl-tooltip.hover - :aria-label="$options.i18n.newIssue" - :title="$options.i18n.newIssue" + :class="{ + 'gl-display-none': !list.isExpanded, + }" + :aria-label="__('New issue')" + :title="__('New issue')" class="issue-count-badge-add-button no-drag" icon="plus" @click="showNewIssueForm" @@ -346,13 +327,13 @@ export default { v-if="isSettingsShown" ref="settingsBtn" v-gl-tooltip.hover - :aria-label="$options.i18n.listSettings" + :aria-label="__('List settings')" class="no-drag js-board-settings-button" - :title="$options.i18n.listSettings" + :title="__('List settings')" icon="settings" @click="openSidebarSettings" /> - <gl-tooltip :target="() => $refs.settingsBtn">{{ $options.i18n.listSettings }}</gl-tooltip> + <gl-tooltip :target="() => $refs.settingsBtn">{{ __('List settings') }}</gl-tooltip> </gl-button-group> </h3> </header> diff --git a/app/assets/javascripts/boards/components/board_list_new.vue b/app/assets/javascripts/boards/components/board_list_new.vue deleted file mode 100644 index 92a381a8f57..00000000000 --- a/app/assets/javascripts/boards/components/board_list_new.vue +++ /dev/null @@ -1,239 +0,0 @@ -<script> -import Draggable from 'vuedraggable'; -import { mapActions, mapState } from 'vuex'; -import { GlLoadingIcon } from '@gitlab/ui'; -import defaultSortableConfig from '~/sortable/sortable_config'; -import { sortableStart, sortableEnd } from '~/boards/mixins/sortable_default_options'; -import BoardNewIssue from './board_new_issue_new.vue'; -import BoardCard from './board_card.vue'; -import eventHub from '../eventhub'; -import { sprintf, __ } from '~/locale'; - -export default { - name: 'BoardList', - i18n: { - loadingIssues: __('Loading issues'), - loadingMoreissues: __('Loading more issues'), - showingAllIssues: __('Showing all issues'), - }, - components: { - BoardCard, - BoardNewIssue, - GlLoadingIcon, - }, - props: { - disabled: { - type: Boolean, - required: true, - }, - list: { - type: Object, - required: true, - }, - issues: { - type: Array, - required: true, - }, - canAdminList: { - type: Boolean, - required: false, - default: false, - }, - }, - data() { - return { - scrollOffset: 250, - showCount: false, - showIssueForm: false, - }; - }, - computed: { - ...mapState(['pageInfoByListId', 'listsFlags']), - paginatedIssueText() { - return sprintf(__('Showing %{pageSize} of %{total} issues'), { - pageSize: this.issues.length, - total: this.list.issuesCount, - }); - }, - issuesSizeExceedsMax() { - return this.list.maxIssueCount > 0 && this.list.issuesCount > this.list.maxIssueCount; - }, - hasNextPage() { - return this.pageInfoByListId[this.list.id].hasNextPage; - }, - loading() { - return this.listsFlags[this.list.id]?.isLoading; - }, - loadingMore() { - return this.listsFlags[this.list.id]?.isLoadingMore; - }, - listRef() { - // When list is draggable, the reference to the list needs to be accessed differently - return this.canAdminList ? this.$refs.list.$el : this.$refs.list; - }, - showingAllIssues() { - return this.issues.length === this.list.issuesCount; - }, - treeRootWrapper() { - return this.canAdminList ? Draggable : 'ul'; - }, - treeRootOptions() { - const options = { - ...defaultSortableConfig, - fallbackOnBody: false, - group: 'board-list', - tag: 'ul', - 'ghost-class': 'board-card-drag-active', - 'data-list-id': this.list.id, - value: this.issues, - }; - - return this.canAdminList ? options : {}; - }, - }, - watch: { - issues() { - this.$nextTick(() => { - this.showCount = this.scrollHeight() > Math.ceil(this.listHeight()); - }); - }, - }, - created() { - eventHub.$on(`toggle-issue-form-${this.list.id}`, this.toggleForm); - eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop); - }, - mounted() { - // Scroll event on list to load more - this.listRef.addEventListener('scroll', this.onScroll); - }, - beforeDestroy() { - eventHub.$off(`toggle-issue-form-${this.list.id}`, this.toggleForm); - eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop); - this.listRef.removeEventListener('scroll', this.onScroll); - }, - methods: { - ...mapActions(['fetchIssuesForList', 'moveIssue']), - listHeight() { - return this.listRef.getBoundingClientRect().height; - }, - scrollHeight() { - return this.listRef.scrollHeight; - }, - scrollTop() { - return this.listRef.scrollTop + this.listHeight(); - }, - scrollToTop() { - this.listRef.scrollTop = 0; - }, - loadNextPage() { - this.fetchIssuesForList({ listId: this.list.id, fetchNext: true }); - }, - toggleForm() { - this.showIssueForm = !this.showIssueForm; - }, - onScroll() { - window.requestAnimationFrame(() => { - if ( - !this.loadingMore && - this.scrollTop() > this.scrollHeight() - this.scrollOffset && - this.hasNextPage - ) { - this.loadNextPage(); - } - }); - }, - handleDragOnStart() { - sortableStart(); - }, - handleDragOnEnd(params) { - sortableEnd(); - const { newIndex, oldIndex, from, to, item } = params; - const { issueId, issueIid, issuePath } = item.dataset; - const { children } = to; - let moveBeforeId; - let moveAfterId; - - const getIssueId = el => Number(el.dataset.issueId); - - // If issue is being moved within the same list - if (from === to) { - if (newIndex > oldIndex && children.length > 1) { - // If issue is being moved down we look for the issue that ends up before - moveBeforeId = getIssueId(children[newIndex]); - } else if (newIndex < oldIndex && children.length > 1) { - // If issue is being moved up we look for the issue that ends up after - moveAfterId = getIssueId(children[newIndex]); - } else { - // If issue remains in the same list at the same position we do nothing - return; - } - } else { - // We look for the issue that ends up before the moved issue if it exists - if (children[newIndex - 1]) { - moveBeforeId = getIssueId(children[newIndex - 1]); - } - // We look for the issue that ends up after the moved issue if it exists - if (children[newIndex]) { - moveAfterId = getIssueId(children[newIndex]); - } - } - - this.moveIssue({ - issueId, - issueIid, - issuePath, - fromListId: from.dataset.listId, - toListId: to.dataset.listId, - moveBeforeId, - moveAfterId, - }); - }, - }, -}; -</script> - -<template> - <div - v-show="!list.collapsed" - class="board-list-component gl-relative gl-h-full gl-display-flex gl-flex-direction-column" - data-qa-selector="board_list_cards_area" - > - <div - v-if="loading" - class="gl-mt-4 gl-text-center" - :aria-label="$options.i18n.loadingIssues" - data-testid="board_list_loading" - > - <gl-loading-icon /> - </div> - <board-new-issue v-if="list.listType !== 'closed' && showIssueForm" :list="list" /> - <component - :is="treeRootWrapper" - v-show="!loading" - ref="list" - v-bind="treeRootOptions" - :data-board="list.id" - :data-board-type="list.listType" - :class="{ 'bg-danger-100': issuesSizeExceedsMax }" - class="board-list gl-w-full gl-h-full gl-list-style-none gl-mb-0 gl-p-2 js-board-list" - data-testid="tree-root-wrapper" - @start="handleDragOnStart" - @end="handleDragOnEnd" - > - <board-card - v-for="(issue, index) in issues" - ref="issue" - :key="issue.id" - :index="index" - :list="list" - :issue="issue" - :disabled="disabled" - /> - <li v-if="showCount" class="board-list-count gl-text-center" data-issue-id="-1"> - <gl-loading-icon v-if="loadingMore" :label="$options.i18n.loadingMoreissues" /> - <span v-if="showingAllIssues">{{ $options.i18n.showingAllIssues }}</span> - <span v-else>{{ paginatedIssueText }}</span> - </li> - </component> - </div> -</template> diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index a9e6d768656..14d28643046 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -1,94 +1,85 @@ <script> +import { mapActions, mapState } from 'vuex'; import { GlButton } from '@gitlab/ui'; import { getMilestone } from 'ee_else_ce/boards/boards_util'; -import ListIssue from 'ee_else_ce/boards/models/issue'; import eventHub from '../eventhub'; import ProjectSelect from './project_select.vue'; -import boardsStore from '../stores/boards_store'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; - -// This component is being replaced in favor of './board_new_issue_new.vue' for GraphQL boards +import { __ } from '~/locale'; export default { name: 'BoardNewIssue', + i18n: { + submit: __('Submit issue'), + cancel: __('Cancel'), + }, components: { ProjectSelect, GlButton, }, mixins: [glFeatureFlagMixin()], + inject: ['groupId', 'weightFeatureAvailable', 'boardWeight'], props: { list: { type: Object, required: true, }, }, - inject: ['groupId'], data() { return { title: '', - error: false, - selectedProject: {}, }; }, computed: { + ...mapState(['selectedProject']), disabled() { if (this.groupId) { return this.title === '' || !this.selectedProject.name; } return this.title === ''; }, + inputFieldId() { + // eslint-disable-next-line @gitlab/require-i18n-strings + return `${this.list.id}-title`; + }, }, mounted() { this.$refs.input.focus(); eventHub.$on('setSelectedProject', this.setSelectedProject); }, methods: { + ...mapActions(['addListNewIssue']), submit(e) { e.preventDefault(); - if (this.title.trim() === '') return Promise.resolve(); - - this.error = false; const labels = this.list.label ? [this.list.label] : []; const assignees = this.list.assignee ? [this.list.assignee] : []; const milestone = getMilestone(this.list); - const { weightFeatureAvailable } = boardsStore; - const { weight } = weightFeatureAvailable ? boardsStore.state.currentBoard : {}; + const weight = this.weightFeatureAvailable ? this.boardWeight : undefined; - const issue = new ListIssue({ - title: this.title, - labels, - subscribed: true, - assignees, - milestone, - project_id: this.selectedProject.id, - weight, - }); + const { title } = this; eventHub.$emit(`scroll-board-list-${this.list.id}`); - this.cancel(); - return this.list - .newIssue(issue) - .then(() => { - boardsStore.setIssueDetail(issue); - boardsStore.setListDetail(this.list); - }) - .catch(() => { - this.list.removeIssue(issue); - - // Show error message - this.error = true; - }); + return this.addListNewIssue({ + issueInput: { + title, + labelIds: labels?.map((l) => l.id), + assigneeIds: assignees?.map((a) => a?.id), + milestoneId: milestone?.id, + projectPath: this.selectedProject.fullPath, + weight: weight >= 0 ? weight : null, + }, + list: this.list, + }).then(() => { + this.reset(); + }); }, - cancel() { + reset() { this.title = ''; eventHub.$emit(`toggle-issue-form-${this.list.id}`); }, - setSelectedProject(selectedProject) { - this.selectedProject = selectedProject; - }, }, }; </script> @@ -96,13 +87,10 @@ export default { <template> <div class="board-new-issue-form"> <div class="board-card position-relative p-3 rounded"> - <form @submit="submit($event)"> - <div v-if="error" class="flash-container"> - <div class="flash-alert">{{ __('An error occurred. Please try again.') }}</div> - </div> - <label :for="list.id + '-title'" class="label-bold">{{ __('Title') }}</label> + <form ref="submitForm" @submit="submit"> + <label :for="inputFieldId" class="label-bold">{{ __('Title') }}</label> <input - :id="list.id + '-title'" + :id="inputFieldId" ref="input" v-model="title" class="form-control" @@ -119,16 +107,18 @@ export default { variant="success" category="primary" type="submit" - >{{ __('Submit issue') }}</gl-button > + {{ $options.i18n.submit }} + </gl-button> <gl-button ref="cancelButton" class="float-right" type="button" variant="default" - @click="cancel" - >{{ __('Cancel') }}</gl-button + @click="reset" > + {{ $options.i18n.cancel }} + </gl-button> </div> </form> </div> diff --git a/app/assets/javascripts/boards/components/board_new_issue_new.vue b/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue index 969c84ddb59..4fc58742783 100644 --- a/app/assets/javascripts/boards/components/board_new_issue_new.vue +++ b/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue @@ -1,33 +1,32 @@ <script> -import { mapActions } from 'vuex'; import { GlButton } from '@gitlab/ui'; import { getMilestone } from 'ee_else_ce/boards/boards_util'; +import ListIssue from 'ee_else_ce/boards/models/issue'; import eventHub from '../eventhub'; -import ProjectSelect from './project_select.vue'; +import ProjectSelect from './project_select_deprecated.vue'; +import boardsStore from '../stores/boards_store'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { __ } from '~/locale'; + +// This component is being replaced in favor of './board_new_issue.vue' for GraphQL boards export default { name: 'BoardNewIssue', - i18n: { - submit: __('Submit issue'), - cancel: __('Cancel'), - }, components: { ProjectSelect, GlButton, }, mixins: [glFeatureFlagMixin()], + inject: ['groupId'], props: { list: { type: Object, required: true, }, }, - inject: ['groupId', 'weightFeatureAvailable', 'boardWeight'], data() { return { title: '', + error: false, selectedProject: {}, }; }, @@ -38,45 +37,52 @@ export default { } return this.title === ''; }, - inputFieldId() { - // eslint-disable-next-line @gitlab/require-i18n-strings - return `${this.list.id}-title`; - }, }, mounted() { this.$refs.input.focus(); eventHub.$on('setSelectedProject', this.setSelectedProject); }, methods: { - ...mapActions(['addListNewIssue']), submit(e) { e.preventDefault(); + if (this.title.trim() === '') return Promise.resolve(); + + this.error = false; const labels = this.list.label ? [this.list.label] : []; const assignees = this.list.assignee ? [this.list.assignee] : []; const milestone = getMilestone(this.list); - const weight = this.weightFeatureAvailable ? this.boardWeight : undefined; + const { weightFeatureAvailable } = boardsStore; + const { weight } = weightFeatureAvailable ? boardsStore.state.currentBoard : {}; - const { title } = this; + const issue = new ListIssue({ + title: this.title, + labels, + subscribed: true, + assignees, + milestone, + project_id: this.selectedProject.id, + weight, + }); eventHub.$emit(`scroll-board-list-${this.list.id}`); + this.cancel(); - return this.addListNewIssue({ - issueInput: { - title, - labelIds: labels?.map(l => l.id), - assigneeIds: assignees?.map(a => a?.id), - milestoneId: milestone?.id, - projectPath: this.selectedProject.path, - weight: weight >= 0 ? weight : null, - }, - list: this.list, - }).then(() => { - this.reset(); - }); + return this.list + .newIssue(issue) + .then(() => { + boardsStore.setIssueDetail(issue); + boardsStore.setListDetail(this.list); + }) + .catch(() => { + this.list.removeIssue(issue); + + // Show error message + this.error = true; + }); }, - reset() { + cancel() { this.title = ''; eventHub.$emit(`toggle-issue-form-${this.list.id}`); }, @@ -90,10 +96,13 @@ export default { <template> <div class="board-new-issue-form"> <div class="board-card position-relative p-3 rounded"> - <form ref="submitForm" @submit="submit"> - <label :for="inputFieldId" class="label-bold">{{ __('Title') }}</label> + <form @submit="submit($event)"> + <div v-if="error" class="flash-container"> + <div class="flash-alert">{{ __('An error occurred. Please try again.') }}</div> + </div> + <label :for="list.id + '-title'" class="label-bold">{{ __('Title') }}</label> <input - :id="inputFieldId" + :id="list.id + '-title'" ref="input" v-model="title" class="form-control" @@ -110,18 +119,16 @@ export default { variant="success" category="primary" type="submit" + >{{ __('Submit issue') }}</gl-button > - {{ $options.i18n.submit }} - </gl-button> <gl-button ref="cancelButton" class="float-right" type="button" variant="default" - @click="reset" + @click="cancel" + >{{ __('Cancel') }}</gl-button > - {{ $options.i18n.cancel }} - </gl-button> </div> </form> </div> diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue index 60db8fefe82..f362fc60bd3 100644 --- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue @@ -116,7 +116,7 @@ export default { v-if="isWipLimitsOn" :max-issue-count="activeList.maxIssueCount" /> - <div v-if="canAdminList && !activeList.preset && activeList.id" class="gl-m-4"> + <div v-if="canAdminList && !activeList.preset && activeList.id" class="gl-mt-4"> <gl-button variant="danger" category="secondary" diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index d26f15c1723..bf3dc5c608f 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -68,7 +68,7 @@ export default Vue.extend({ : __('Label'); }, selectedLabels() { - return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : ''; + return this.hasLabels ? this.issue.labels.map((l) => l.title).join(',') : ''; }, }, watch: { @@ -82,9 +82,7 @@ export default Vue.extend({ }); $('.js-issue-board-sidebar', this.$el).each((i, el) => { - $(el) - .data('deprecatedJQueryDropdown') - .clearMenu(); + $(el).data('deprecatedJQueryDropdown').clearMenu(); }); } diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue index 4f23c38d0f7..fcd1c3fdceb 100644 --- a/app/assets/javascripts/boards/components/boards_selector.vue +++ b/app/assets/javascripts/boards/components/boards_selector.vue @@ -118,7 +118,7 @@ export default { return this.state.currentPage; }, filteredBoards() { - return this.boards.filter(board => + return this.boards.filter((board) => board.name.toLowerCase().includes(this.filterTerm.toLowerCase()), ); }, @@ -181,10 +181,10 @@ export default { this.loadingRecentBoards = true; boardsStore .recentBoards() - .then(res => { + .then((res) => { this.recentBoards = res.data; }) - .catch(err => { + .catch((err) => { /** * If user is unauthorized we'd still want to resolve the * request to display all boards. diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index ddd20ff281c..457d0d4dcd6 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -1,6 +1,6 @@ <script> import { sortBy } from 'lodash'; -import { mapState } from 'vuex'; +import { mapActions, mapState } from 'vuex'; import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner'; import { sprintf, __, n__ } from '~/locale'; @@ -8,9 +8,10 @@ import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import IssueDueDate from './issue_due_date.vue'; import IssueTimeEstimate from './issue_time_estimate.vue'; -import boardsStore from '../stores/boards_store'; +import eventHub from '../eventhub'; import { isScopedLabel } from '~/lib/utils/common_utils'; import { ListType } from '../constants'; +import { updateHistory } from '~/lib/utils/url_utility'; export default { components: { @@ -26,6 +27,7 @@ export default { GlTooltip: GlTooltipDirective, }, mixins: [issueCardInner], + inject: ['groupId', 'rootPath', 'scopedLabelsAvailable'], props: { issue: { type: Object, @@ -42,7 +44,6 @@ export default { default: false, }, }, - inject: ['groupId', 'rootPath'], data() { return { limitBeforeCounter: 2, @@ -52,6 +53,16 @@ export default { }, computed: { ...mapState(['isShowingLabels']), + cappedAssignees() { + // e.g. maxRender is 4, + // Render up to all 4 assignees if there are only 4 assigness + // Otherwise render up to the limitBeforeCounter + if (this.issue.assignees.length <= this.maxRender) { + return this.issue.assignees.slice(0, this.maxRender); + } + + return this.issue.assignees.slice(0, this.limitBeforeCounter); + }, numberOverLimit() { return this.issue.assignees.length - this.limitBeforeCounter; }, @@ -98,19 +109,10 @@ export default { }, }, methods: { + ...mapActions(['performSearch']), isIndexLessThanlimit(index) { return index < this.limitBeforeCounter; }, - shouldRenderAssignee(index) { - // Eg. maxRender is 4, - // Render up to all 4 assignees if there are only 4 assigness - // Otherwise render up to the limitBeforeCounter - if (this.issue.assignees.length <= this.maxRender) { - return index < this.maxRender; - } - - return index < this.limitBeforeCounter; - }, assigneeUrl(assignee) { if (!assignee) return ''; return `${this.rootPath}${assignee.username}`; @@ -118,6 +120,9 @@ export default { avatarUrlTitle(assignee) { return sprintf(__(`Avatar for %{assigneeName}`), { assigneeName: assignee.name }); }, + avatarUrl(assignee) { + return assignee.avatarUrl || assignee.avatar || gon.default_avatar_url; + }, showLabel(label) { if (!label.id) return false; return true; @@ -133,13 +138,19 @@ export default { }, filterByLabel(label) { if (!this.updateFilters) return; - const labelTitle = encodeURIComponent(label.title); - const filter = `label_name[]=${labelTitle}`; + const filterPath = window.location.search ? `${window.location.search}&` : '?'; + const filter = `label_name[]=${encodeURIComponent(label.title)}`; - boardsStore.toggleFilter(filter); + if (!filterPath.includes(filter)) { + updateHistory({ + url: `${filterPath}${filter}`, + }); + this.performSearch(); + eventHub.$emit('updateTokens'); + } }, showScopedLabel(label) { - return boardsStore.scopedLabels.enabled && isScopedLabel(label); + return this.scopedLabelsAvailable && isScopedLabel(label); }, }, }; @@ -222,12 +233,11 @@ export default { </div> <div class="board-card-assignee gl-display-flex"> <user-avatar-link - v-for="(assignee, index) in issue.assignees" - v-if="shouldRenderAssignee(index)" + v-for="assignee in cappedAssignees" :key="assignee.id" :link-href="assigneeUrl(assignee)" :img-alt="avatarUrlTitle(assignee)" - :img-src="assignee.avatarUrl || assignee.avatar || assignee.avatar_url" + :img-src="avatarUrl(assignee)" :img-size="24" class="js-no-trigger" tooltip-placement="bottom" diff --git a/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue b/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue new file mode 100644 index 00000000000..75cf1f0b9e1 --- /dev/null +++ b/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue @@ -0,0 +1,245 @@ +<script> +import { sortBy } from 'lodash'; +import { mapState } from 'vuex'; +import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui'; +import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner'; +import { sprintf, __, n__ } from '~/locale'; +import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; +import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; +import IssueDueDate from './issue_due_date.vue'; +import IssueTimeEstimate from './issue_time_estimate_deprecated.vue'; +import boardsStore from '../stores/boards_store'; +import { isScopedLabel } from '~/lib/utils/common_utils'; + +export default { + components: { + GlLabel, + GlIcon, + UserAvatarLink, + TooltipOnTruncate, + IssueDueDate, + IssueTimeEstimate, + IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'), + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + mixins: [issueCardInner], + inject: ['groupId', 'rootPath'], + props: { + issue: { + type: Object, + required: true, + }, + list: { + type: Object, + required: false, + default: () => ({}), + }, + updateFilters: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + limitBeforeCounter: 2, + maxRender: 3, + maxCounter: 99, + }; + }, + computed: { + ...mapState(['isShowingLabels']), + numberOverLimit() { + return this.issue.assignees.length - this.limitBeforeCounter; + }, + assigneeCounterTooltip() { + const { numberOverLimit, maxCounter } = this; + const count = numberOverLimit > maxCounter ? maxCounter : numberOverLimit; + return sprintf(__('%{count} more assignees'), { count }); + }, + assigneeCounterLabel() { + if (this.numberOverLimit > this.maxCounter) { + return `${this.maxCounter}+`; + } + + return `+${this.numberOverLimit}`; + }, + shouldRenderCounter() { + if (this.issue.assignees.length <= this.maxRender) { + return false; + } + + return this.issue.assignees.length > this.numberOverLimit; + }, + issueId() { + if (this.issue.iid) { + return `#${this.issue.iid}`; + } + return false; + }, + showLabelFooter() { + return this.isShowingLabels && this.issue.labels.find(this.showLabel); + }, + issueReferencePath() { + const { referencePath, groupId } = this.issue; + return !groupId ? referencePath.split('#')[0] : null; + }, + orderedLabels() { + return sortBy(this.issue.labels.filter(this.isNonListLabel), 'title'); + }, + blockedLabel() { + if (this.issue.blockedByCount) { + return n__(`Blocked by %d issue`, `Blocked by %d issues`, this.issue.blockedByCount); + } + return __('Blocked issue'); + }, + }, + methods: { + isIndexLessThanlimit(index) { + return index < this.limitBeforeCounter; + }, + shouldRenderAssignee(index) { + // Eg. maxRender is 4, + // Render up to all 4 assignees if there are only 4 assigness + // Otherwise render up to the limitBeforeCounter + if (this.issue.assignees.length <= this.maxRender) { + return index < this.maxRender; + } + + return index < this.limitBeforeCounter; + }, + assigneeUrl(assignee) { + if (!assignee) return ''; + return `${this.rootPath}${assignee.username}`; + }, + avatarUrlTitle(assignee) { + return sprintf(__(`Avatar for %{assigneeName}`), { assigneeName: assignee.name }); + }, + showLabel(label) { + if (!label.id) return false; + return true; + }, + isNonListLabel(label) { + return label.id && !(this.list.type === 'label' && this.list.title === label.title); + }, + filterByLabel(label) { + if (!this.updateFilters) return; + const labelTitle = encodeURIComponent(label.title); + const filter = `label_name[]=${labelTitle}`; + + boardsStore.toggleFilter(filter); + }, + showScopedLabel(label) { + return boardsStore.scopedLabels.enabled && isScopedLabel(label); + }, + }, +}; +</script> +<template> + <div> + <div class="gl-display-flex" dir="auto"> + <h4 class="board-card-title gl-mb-0 gl-mt-0"> + <gl-icon + v-if="issue.blocked" + v-gl-tooltip + name="issue-block" + :title="blockedLabel" + class="issue-blocked-icon gl-mr-2" + :aria-label="blockedLabel" + data-testid="issue-blocked-icon" + /> + <gl-icon + v-if="issue.confidential" + v-gl-tooltip + name="eye-slash" + :title="__('Confidential')" + class="confidential-icon gl-mr-2" + :aria-label="__('Confidential')" + /> + <a + :href="issue.path || issue.webUrl || ''" + :title="issue.title" + class="js-no-trigger" + @mousemove.stop + >{{ issue.title }}</a + > + </h4> + </div> + <div v-if="showLabelFooter" class="board-card-labels gl-mt-2 gl-display-flex gl-flex-wrap"> + <template v-for="label in orderedLabels"> + <gl-label + :key="label.id" + :background-color="label.color" + :title="label.title" + :description="label.description" + size="sm" + :scoped="showScopedLabel(label)" + @click="filterByLabel(label)" + /> + </template> + </div> + <div + class="board-card-footer gl-display-flex gl-justify-content-space-between gl-align-items-flex-end" + > + <div + class="gl-display-flex align-items-start flex-wrap-reverse board-card-number-container gl-overflow-hidden js-board-card-number-container" + > + <span + v-if="issue.referencePath" + class="board-card-number gl-overflow-hidden gl-display-flex gl-mr-3 gl-mt-3" + > + <tooltip-on-truncate + v-if="issueReferencePath" + :title="issueReferencePath" + placement="bottom" + class="board-issue-path gl-text-truncate gl-font-weight-bold" + >{{ issueReferencePath }}</tooltip-on-truncate + > + #{{ issue.iid }} + </span> + <span class="board-info-items gl-mt-3 gl-display-inline-block"> + <issue-due-date + v-if="issue.dueDate" + :date="issue.dueDate" + :closed="issue.closed || Boolean(issue.closedAt)" + /> + <issue-time-estimate v-if="issue.timeEstimate" :estimate="issue.timeEstimate" /> + <issue-card-weight + v-if="validIssueWeight" + :weight="issue.weight" + @click="filterByWeight(issue.weight)" + /> + </span> + </div> + <div class="board-card-assignee gl-display-flex"> + <user-avatar-link + v-for="(assignee, index) in issue.assignees" + v-if="shouldRenderAssignee(index)" + :key="assignee.id" + :link-href="assigneeUrl(assignee)" + :img-alt="avatarUrlTitle(assignee)" + :img-src="assignee.avatarUrl || assignee.avatar || assignee.avatar_url" + :img-size="24" + class="js-no-trigger" + tooltip-placement="bottom" + > + <span class="js-assignee-tooltip"> + <span class="gl-font-weight-bold gl-display-block">{{ __('Assignee') }}</span> + {{ assignee.name }} + <span class="text-white-50">@{{ assignee.username }}</span> + </span> + </user-avatar-link> + <span + v-if="shouldRenderCounter" + v-gl-tooltip + :title="assigneeCounterTooltip" + class="avatar-counter" + data-placement="bottom" + >{{ assigneeCounterLabel }}</span + > + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/issue_time_estimate.vue b/app/assets/javascripts/boards/components/issue_time_estimate.vue index fe56833016e..f6b00b695da 100644 --- a/app/assets/javascripts/boards/components/issue_time_estimate.vue +++ b/app/assets/javascripts/boards/components/issue_time_estimate.vue @@ -1,30 +1,34 @@ <script> import { GlTooltip, GlIcon } from '@gitlab/ui'; +import { __ } from '~/locale'; import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility'; -import boardsStore from '../stores/boards_store'; export default { + i18n: { + timeEstimate: __('Time estimate'), + }, components: { GlIcon, GlTooltip, }, + inject: ['timeTrackingLimitToHours'], props: { estimate: { type: Number, required: true, }, }, - data() { - return { - limitToHours: boardsStore.timeTracking.limitToHours, - }; - }, computed: { title() { - return stringifyTime(parseSeconds(this.estimate, { limitToHours: this.limitToHours }), true); + return stringifyTime( + parseSeconds(this.estimate, { limitToHours: this.timeTrackingLimitToHours }), + true, + ); }, timeEstimate() { - return stringifyTime(parseSeconds(this.estimate, { limitToHours: this.limitToHours })); + return stringifyTime( + parseSeconds(this.estimate, { limitToHours: this.timeTrackingLimitToHours }), + ); }, }, }; @@ -33,16 +37,16 @@ export default { <template> <span> <span ref="issueTimeEstimate" class="board-card-info card-number"> - <gl-icon name="hourglass" class="board-card-info-icon" /><time class="board-card-info-text">{{ - timeEstimate - }}</time> + <gl-icon name="hourglass" class="board-card-info-icon" /> + <time class="board-card-info-text">{{ timeEstimate }}</time> </span> <gl-tooltip :target="() => $refs.issueTimeEstimate" placement="bottom" class="js-issue-time-estimate" > - <span class="bold d-block">{{ __('Time estimate') }}</span> {{ title }} + <span class="gl-font-weight-bold gl-display-block">{{ $options.i18n.timeEstimate }}</span> + {{ title }} </gl-tooltip> </span> </template> diff --git a/app/assets/javascripts/boards/components/issue_time_estimate_deprecated.vue b/app/assets/javascripts/boards/components/issue_time_estimate_deprecated.vue new file mode 100644 index 00000000000..fe56833016e --- /dev/null +++ b/app/assets/javascripts/boards/components/issue_time_estimate_deprecated.vue @@ -0,0 +1,48 @@ +<script> +import { GlTooltip, GlIcon } from '@gitlab/ui'; +import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility'; +import boardsStore from '../stores/boards_store'; + +export default { + components: { + GlIcon, + GlTooltip, + }, + props: { + estimate: { + type: Number, + required: true, + }, + }, + data() { + return { + limitToHours: boardsStore.timeTracking.limitToHours, + }; + }, + computed: { + title() { + return stringifyTime(parseSeconds(this.estimate, { limitToHours: this.limitToHours }), true); + }, + timeEstimate() { + return stringifyTime(parseSeconds(this.estimate, { limitToHours: this.limitToHours })); + }, + }, +}; +</script> + +<template> + <span> + <span ref="issueTimeEstimate" class="board-card-info card-number"> + <gl-icon name="hourglass" class="board-card-info-icon" /><time class="board-card-info-text">{{ + timeEstimate + }}</time> + </span> + <gl-tooltip + :target="() => $refs.issueTimeEstimate" + placement="bottom" + class="js-issue-time-estimate" + > + <span class="bold d-block">{{ __('Time estimate') }}</span> {{ title }} + </gl-tooltip> + </span> +</template> diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue index d28a03da97f..10c29977cae 100644 --- a/app/assets/javascripts/boards/components/modal/footer.vue +++ b/app/assets/javascripts/boards/components/modal/footer.vue @@ -40,21 +40,21 @@ export default { const firstListIndex = 1; const list = this.modal.selectedList || this.state.lists[firstListIndex]; const selectedIssues = ModalStore.getSelectedIssues(); - const issueIds = selectedIssues.map(issue => issue.id); + const issueIds = selectedIssues.map((issue) => issue.id); const req = this.buildUpdateRequest(list); // Post the data to the backend boardsStore.bulkUpdate(issueIds, req).catch(() => { Flash(__('Failed to update issues, please try again.')); - selectedIssues.forEach(issue => { + selectedIssues.forEach((issue) => { list.removeIssue(issue); list.issuesSize -= 1; }); }); // Add the issues on the frontend - selectedIssues.forEach(issue => { + selectedIssues.forEach((issue) => { list.addIssue(issue); list.issuesSize += 1; }); diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue index 817b3bdddb0..84d687a46b9 100644 --- a/app/assets/javascripts/boards/components/modal/index.vue +++ b/app/assets/javascripts/boards/components/modal/index.vue @@ -65,9 +65,7 @@ export default { this.loading = false; }; - this.loadIssues() - .then(loadingDone) - .catch(loadingDone); + this.loadIssues().then(loadingDone).catch(loadingDone); } else if (!this.showAddIssuesModal) { this.issues = []; this.selectedIssues = []; @@ -83,9 +81,7 @@ export default { this.filterLoading = false; }; - this.loadIssues(true) - .then(loadingDone) - .catch(loadingDone); + this.loadIssues(true).then(loadingDone).catch(loadingDone); } }, deep: true, @@ -104,13 +100,13 @@ export default { page: this.page, per: this.perPage, }) - .then(res => res.data) - .then(data => { + .then((res) => res.data) + .then((data) => { if (clearIssues) { this.issues = []; } - data.issues.forEach(issueObj => { + data.issues.forEach((issueObj) => { const issue = new ListIssue(issueObj); const foundSelectedIssue = ModalStore.findSelectedIssue(issue); issue.selected = Boolean(foundSelectedIssue); diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index d1011c24977..2bc54155163 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -40,7 +40,7 @@ $(document) }); export default function initNewListDropdown() { - $('.js-new-board-list').each(function() { + $('.js-new-board-list').each(function () { const $dropdownToggle = $(this); const $dropdown = $dropdownToggle.closest('.dropdown'); new CreateLabelDropdown( diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue index 9c90938fc52..04699d0d3a4 100644 --- a/app/assets/javascripts/boards/components/project_select.vue +++ b/app/assets/javascripts/boards/components/project_select.vue @@ -1,120 +1,141 @@ <script> -import $ from 'jquery'; -import { escape } from 'lodash'; -import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; -import { __ } from '~/locale'; -import eventHub from '../eventhub'; -import Api from '../../api'; +import { mapActions, mapState } from 'vuex'; +import { + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlSearchBoxByType, + GlIntersectionObserver, + GlLoadingIcon, +} from '@gitlab/ui'; +import { s__ } from '~/locale'; import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants'; -import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; import { ListType } from '../constants'; export default { - name: 'BoardProjectSelect', + name: 'ProjectSelect', + i18n: { + headerTitle: s__(`BoardNewIssue|Projects`), + dropdownText: s__(`BoardNewIssue|Select a project`), + searchPlaceholder: s__(`BoardNewIssue|Search projects`), + emptySearchResult: s__(`BoardNewIssue|No matching results`), + }, + defaultFetchOptions: { + with_issues_enabled: true, + with_shared: false, + include_subgroups: true, + order_by: 'similarity', + }, components: { - GlIcon, + GlIntersectionObserver, GlLoadingIcon, + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlSearchBoxByType, }, + inject: ['groupId'], props: { list: { type: Object, required: true, }, }, - inject: ['groupId'], data() { return { - loading: true, + initialLoading: true, selectedProject: {}, + searchTerm: '', }; }, computed: { + ...mapState(['groupProjects', 'groupProjectsFlags']), selectedProjectName() { - return this.selectedProject.name || __('Select a project'); + return this.selectedProject.name || this.$options.i18n.dropdownText; + }, + fetchOptions() { + const additionalAttrs = {}; + if (this.list.type && this.list.type !== ListType.backlog) { + additionalAttrs.min_access_level = featureAccessLevel.EVERYONE; + } + + return { + ...this.$options.defaultFetchOptions, + ...additionalAttrs, + }; + }, + isFetchResultEmpty() { + return this.groupProjects.length === 0; + }, + hasNextPage() { + return this.groupProjectsFlags.pageInfo?.hasNextPage; + }, + }, + watch: { + searchTerm() { + this.fetchGroupProjects({ search: this.searchTerm }); }, }, mounted() { - initDeprecatedJQueryDropdown($(this.$refs.projectsDropdown), { - filterable: true, - filterRemote: true, - search: { - fields: ['name_with_namespace'], - }, - clicked: ({ $el, e }) => { - e.preventDefault(); - this.selectedProject = { - id: $el.data('project-id'), - name: $el.data('project-name'), - path: $el.data('project-path'), - }; - eventHub.$emit('setSelectedProject', this.selectedProject); - }, - selectable: true, - data: (term, callback) => { - this.loading = true; - const additionalAttrs = {}; + this.fetchGroupProjects({}); - if ((this.list.type || this.list.listType) !== ListType.backlog) { - additionalAttrs.min_access_level = featureAccessLevel.EVERYONE; - } - - return Api.groupProjects( - this.groupId, - term, - { - with_issues_enabled: true, - with_shared: false, - include_subgroups: true, - order_by: 'similarity', - ...additionalAttrs, - }, - projects => { - this.loading = false; - callback(projects); - }, - ); - }, - renderRow(project) { - return ` - <li> - <a href='#' class='dropdown-menu-link' - data-project-id="${project.id}" - data-project-name="${project.name}" - data-project-name-with-namespace="${project.name_with_namespace}" - data-project-path="${project.path_with_namespace}" - > - ${escape(project.name_with_namespace)} - </a> - </li> - `; - }, - text: project => project.name_with_namespace, - }); + this.initialLoading = false; + }, + methods: { + ...mapActions(['fetchGroupProjects', 'setSelectedProject']), + selectProject(projectId) { + this.selectedProject = this.groupProjects.find((project) => project.id === projectId); + this.setSelectedProject(this.selectedProject); + }, + loadMoreProjects() { + this.fetchGroupProjects({ search: this.searchTerm, fetchNext: true }); + }, }, }; </script> <template> <div> - <label class="label-bold gl-mt-3">{{ __('Project') }}</label> - <div ref="projectsDropdown" class="dropdown dropdown-projects"> - <button - class="dropdown-menu-toggle wide" - type="button" - data-toggle="dropdown" - aria-expanded="false" + <label class="gl-font-weight-bold gl-mt-3" data-testid="header-label">{{ + $options.i18n.headerTitle + }}</label> + <gl-dropdown + data-testid="project-select-dropdown" + :text="selectedProjectName" + :header-text="$options.i18n.headerTitle" + block + menu-class="gl-w-full!" + :loading="initialLoading" + > + <gl-search-box-by-type + v-model.trim="searchTerm" + debounce="250" + :placeholder="$options.i18n.searchPlaceholder" + /> + <gl-dropdown-item + v-for="project in groupProjects" + v-show="!groupProjectsFlags.isLoading" + :key="project.id" + :name="project.name" + @click="selectProject(project.id)" + > + {{ project.nameWithNamespace }} + </gl-dropdown-item> + <gl-dropdown-text + v-show="groupProjectsFlags.isLoading" + data-testid="dropdown-text-loading-icon" + > + <gl-loading-icon class="gl-mx-auto" /> + </gl-dropdown-text> + <gl-dropdown-text + v-if="isFetchResultEmpty && !groupProjectsFlags.isLoading" + data-testid="empty-result-message" > - {{ selectedProjectName }} <gl-icon name="chevron-down" class="dropdown-menu-toggle-icon" /> - </button> - <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width"> - <div class="dropdown-title">{{ __('Projects') }}</div> - <div class="dropdown-input"> - <input class="dropdown-input-field" type="search" :placeholder="__('Search projects')" /> - <gl-icon name="search" class="dropdown-input-search" data-hidden="true" /> - </div> - <div class="dropdown-content"></div> - <div class="dropdown-loading"><gl-loading-icon /></div> - </div> - </div> + <span class="gl-text-gray-500">{{ $options.i18n.emptySearchResult }}</span> + </gl-dropdown-text> + <gl-intersection-observer v-if="hasNextPage" @appear="loadMoreProjects"> + <gl-loading-icon v-if="groupProjectsFlags.isLoadingMore" size="md" /> + </gl-intersection-observer> + </gl-dropdown> </div> </template> diff --git a/app/assets/javascripts/boards/components/project_select_deprecated.vue b/app/assets/javascripts/boards/components/project_select_deprecated.vue new file mode 100644 index 00000000000..a043dc575ca --- /dev/null +++ b/app/assets/javascripts/boards/components/project_select_deprecated.vue @@ -0,0 +1,145 @@ +<script> +import { + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlSearchBoxByType, + GlLoadingIcon, +} from '@gitlab/ui'; +import eventHub from '../eventhub'; +import { s__ } from '~/locale'; +import Api from '../../api'; +import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants'; +import { ListType } from '../constants'; + +export default { + name: 'ProjectSelect', + i18n: { + headerTitle: s__(`BoardNewIssue|Projects`), + dropdownText: s__(`BoardNewIssue|Select a project`), + searchPlaceholder: s__(`BoardNewIssue|Search projects`), + emptySearchResult: s__(`BoardNewIssue|No matching results`), + }, + defaultFetchOptions: { + with_issues_enabled: true, + with_shared: false, + include_subgroups: true, + order_by: 'similarity', + }, + components: { + GlLoadingIcon, + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlSearchBoxByType, + }, + inject: ['groupId'], + props: { + list: { + type: Object, + required: true, + }, + }, + data() { + return { + initialLoading: true, + isFetching: false, + projects: [], + selectedProject: {}, + searchTerm: '', + }; + }, + computed: { + selectedProjectName() { + return this.selectedProject.name || this.$options.i18n.dropdownText; + }, + fetchOptions() { + const additionalAttrs = {}; + if (this.list.type && this.list.type !== ListType.backlog) { + additionalAttrs.min_access_level = featureAccessLevel.EVERYONE; + } + + return { + ...this.$options.defaultFetchOptions, + ...additionalAttrs, + }; + }, + isFetchResultEmpty() { + return this.projects.length === 0; + }, + }, + watch: { + searchTerm() { + this.fetchProjects(); + }, + }, + async mounted() { + await this.fetchProjects(); + + this.initialLoading = false; + }, + methods: { + async fetchProjects() { + this.isFetching = true; + try { + const projects = await Api.groupProjects(this.groupId, this.searchTerm, this.fetchOptions); + + this.projects = projects.map((project) => { + return { + id: project.id, + name: project.name, + namespacedName: project.name_with_namespace, + path: project.path_with_namespace, + }; + }); + } catch (err) { + /* Handled in Api.groupProjects */ + } finally { + this.isFetching = false; + } + }, + selectProject(projectId) { + this.selectedProject = this.projects.find((project) => project.id === projectId); + + eventHub.$emit('setSelectedProject', this.selectedProject); + }, + }, +}; +</script> + +<template> + <div> + <label class="gl-font-weight-bold gl-mt-3" data-testid="header-label">{{ + $options.i18n.headerTitle + }}</label> + <gl-dropdown + data-testid="project-select-dropdown" + :text="selectedProjectName" + :header-text="$options.i18n.headerTitle" + block + menu-class="gl-w-full!" + :loading="initialLoading" + > + <gl-search-box-by-type + v-model.trim="searchTerm" + debounce="250" + :placeholder="$options.i18n.searchPlaceholder" + /> + <gl-dropdown-item + v-for="project in projects" + v-show="!isFetching" + :key="project.id" + :name="project.name" + @click="selectProject(project.id)" + > + {{ project.namespacedName }} + </gl-dropdown-item> + <gl-dropdown-text v-show="isFetching" data-testid="dropdown-text-loading-icon"> + <gl-loading-icon class="gl-mx-auto" /> + </gl-dropdown-text> + <gl-dropdown-text v-if="isFetchResultEmpty && !isFetching" data-testid="empty-result-message"> + <span class="gl-text-gray-500">{{ $options.i18n.emptySearchResult }}</span> + </gl-dropdown-text> + </gl-dropdown> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue index ce267be6d45..61863bbe2a9 100644 --- a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue +++ b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue @@ -3,6 +3,7 @@ import { GlButton, GlLoadingIcon } from '@gitlab/ui'; export default { components: { GlButton, GlLoadingIcon }, + inject: ['canUpdate'], props: { title: { type: String, @@ -14,20 +15,41 @@ export default { required: false, default: false, }, + toggleHeader: { + type: Boolean, + required: false, + default: false, + }, + handleOffClick: { + type: Boolean, + required: false, + default: true, + }, }, - inject: ['canUpdate'], data() { return { edit: false, }; }, + computed: { + showHeader() { + if (!this.toggleHeader) { + return true; + } + + return !this.edit; + }, + }, destroyed() { window.removeEventListener('click', this.collapseWhenOffClick); }, methods: { collapseWhenOffClick({ target }) { if (!this.$el.contains(target)) { - this.collapse(); + this.$emit('off-click'); + if (this.handleOffClick) { + this.collapse(); + } } }, expand() { @@ -63,21 +85,26 @@ export default { <template> <div> - <div class="gl-display-flex gl-justify-content-space-between gl-mb-3"> + <header + v-show="showHeader" + class="gl-display-flex gl-justify-content-space-between gl-align-items-flex-start gl-mb-3" + > <span class="gl-vertical-align-middle"> - <span data-testid="title">{{ title }}</span> + <slot name="title"> + <span data-testid="title">{{ title }}</span> + </slot> <gl-loading-icon v-if="loading" inline class="gl-ml-2" /> </span> <gl-button v-if="canUpdate" variant="link" - class="gl-text-gray-900! js-sidebar-dropdown-toggle" + class="gl-text-gray-900! gl-ml-5 js-sidebar-dropdown-toggle" data-testid="edit-button" @click="toggle" > {{ __('Edit') }} </gl-button> - </div> + </header> <div v-show="!edit" class="gl-text-gray-500" data-testid="collapsed-content"> <slot name="collapsed">{{ __('None') }}</slot> </div> diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue index 904ceaed1b3..4a664d5beef 100644 --- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue +++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue @@ -18,16 +18,16 @@ export default { }; }, computed: { - ...mapGetters({ issue: 'activeIssue', projectPathForActiveIssue: 'projectPathForActiveIssue' }), + ...mapGetters(['activeIssue', 'projectPathForActiveIssue']), hasDueDate() { - return this.issue.dueDate != null; + return this.activeIssue.dueDate != null; }, parsedDueDate() { if (!this.hasDueDate) { return null; } - return parsePikadayDate(this.issue.dueDate); + return parsePikadayDate(this.activeIssue.dueDate); }, formattedDueDate() { if (!this.hasDueDate) { diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue new file mode 100644 index 00000000000..d0e641daf5c --- /dev/null +++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue @@ -0,0 +1,171 @@ +<script> +import { mapGetters, mapActions } from 'vuex'; +import { GlAlert, GlButton, GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui'; +import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; +import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; +import { joinPaths } from '~/lib/utils/url_utility'; +import createFlash from '~/flash'; +import { __ } from '~/locale'; + +export default { + components: { + GlForm, + GlAlert, + GlButton, + GlFormGroup, + GlFormInput, + BoardEditableItem, + }, + directives: { + autofocusonshow, + }, + data() { + return { + title: '', + loading: false, + showChangesAlert: false, + }; + }, + computed: { + ...mapGetters({ issue: 'activeIssue' }), + pendingChangesStorageKey() { + return this.getPendingChangesKey(this.issue); + }, + projectPath() { + const referencePath = this.issue.referencePath || ''; + return referencePath.slice(0, referencePath.indexOf('#')); + }, + validationState() { + return Boolean(this.title); + }, + }, + watch: { + issue: { + handler(updatedIssue, formerIssue) { + if (formerIssue?.title !== this.title) { + localStorage.setItem(this.getPendingChangesKey(formerIssue), this.title); + } + + this.title = updatedIssue.title; + this.setPendingState(); + }, + immediate: true, + }, + }, + methods: { + ...mapActions(['setActiveIssueTitle']), + getPendingChangesKey(issue) { + if (!issue) { + return ''; + } + + return joinPaths( + window.location.pathname.slice(1), + String(issue.id), + 'issue-title-pending-changes', + ); + }, + async setPendingState() { + const pendingChanges = localStorage.getItem(this.pendingChangesStorageKey); + + if (pendingChanges) { + this.title = pendingChanges; + this.showChangesAlert = true; + await this.$nextTick(); + this.$refs.sidebarItem.expand(); + } else { + this.showChangesAlert = false; + } + }, + cancel() { + this.title = this.issue.title; + this.$refs.sidebarItem.collapse(); + this.showChangesAlert = false; + localStorage.removeItem(this.pendingChangesStorageKey); + }, + async setTitle() { + this.$refs.sidebarItem.collapse(); + + if (!this.title || this.title === this.issue.title) { + return; + } + + try { + this.loading = true; + await this.setActiveIssueTitle({ title: this.title, projectPath: this.projectPath }); + localStorage.removeItem(this.pendingChangesStorageKey); + this.showChangesAlert = false; + } catch (e) { + this.title = this.issue.title; + createFlash({ message: this.$options.i18n.updateTitleError }); + } finally { + this.loading = false; + } + }, + handleOffClick() { + if (this.title !== this.issue.title) { + this.showChangesAlert = true; + localStorage.setItem(this.pendingChangesStorageKey, this.title); + } else { + this.$refs.sidebarItem.collapse(); + } + }, + }, + i18n: { + issueTitlePlaceholder: __('Issue title'), + submitButton: __('Save changes'), + cancelButton: __('Cancel'), + updateTitleError: __('An error occurred when updating the issue title'), + invalidFeedback: __('An issue title is required'), + reviewYourChanges: __('Changes to the title have not been saved'), + }, +}; +</script> + +<template> + <board-editable-item + ref="sidebarItem" + toggle-header + :loading="loading" + :handle-off-click="false" + @off-click="handleOffClick" + > + <template #title> + <span class="gl-font-weight-bold" data-testid="issue-title">{{ issue.title }}</span> + </template> + <template #collapsed> + <span class="gl-text-gray-800">{{ issue.referencePath }}</span> + </template> + <template> + <gl-alert v-if="showChangesAlert" variant="warning" class="gl-mb-5" :dismissible="false"> + {{ $options.i18n.reviewYourChanges }} + </gl-alert> + <gl-form @submit.prevent="setTitle"> + <gl-form-group :invalid-feedback="$options.i18n.invalidFeedback" :state="validationState"> + <gl-form-input + v-model="title" + v-autofocusonshow + :placeholder="$options.i18n.issueTitlePlaceholder" + :state="validationState" + /> + </gl-form-group> + + <div class="gl-display-flex gl-w-full gl-justify-content-space-between gl-mt-5"> + <gl-button + variant="success" + size="small" + data-testid="submit-button" + :disabled="!title" + @click="setTitle" + > + {{ $options.i18n.submitButton }} + </gl-button> + + <gl-button size="small" data-testid="cancel-button" @click="cancel"> + {{ $options.i18n.cancelButton }} + </gl-button> + </div> + </gl-form> + </template> + </board-editable-item> +</template> diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue index 6a407bd6ba6..dcf769e6fe5 100644 --- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue +++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue @@ -14,18 +14,18 @@ export default { LabelsSelect, GlLabel, }, + inject: ['labelsFetchPath', 'labelsManagePath', 'labelsFilterBasePath'], data() { return { loading: false, }; }, - inject: ['labelsFetchPath', 'labelsManagePath', 'labelsFilterBasePath'], computed: { ...mapGetters(['activeIssue', 'projectPathForActiveIssue']), selectedLabels() { const { labels = [] } = this.activeIssue; - return labels.map(label => ({ + return labels.map((label) => ({ ...label, id: getIdFromGraphQLId(label.id), })); @@ -33,7 +33,7 @@ export default { issueLabels() { const { labels = [] } = this.activeIssue; - return labels.map(label => ({ + return labels.map((label) => ({ ...label, scoped: isScopedLabel(label), })); @@ -46,10 +46,10 @@ export default { this.$refs.sidebarItem.collapse(); try { - const addLabelIds = payload.filter(label => label.set).map(label => label.id); + const addLabelIds = payload.filter((label) => label.set).map((label) => label.id); const removeLabelIds = this.selectedLabels - .filter(label => !payload.find(selected => selected.id === label.id)) - .map(label => label.id); + .filter((label) => !payload.find((selected) => selected.id === label.id)) + .map((label) => label.id); const input = { addLabelIds, removeLabelIds, projectPath: this.projectPathForActiveIssue }; await this.setActiveIssueLabels(input); diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue index 78c3f8acc62..144a81f009b 100644 --- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue +++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue @@ -50,7 +50,7 @@ export default { }, update(data) { const edges = data?.group?.milestones?.edges ?? []; - return edges.map(item => item.node); + return edges.map((item) => item.node); }, error() { createFlash({ message: this.$options.i18n.fetchMilestonesError }); @@ -58,20 +58,20 @@ export default { }, }, computed: { - ...mapGetters({ issue: 'activeIssue' }), + ...mapGetters(['activeIssue']), hasMilestone() { - return this.issue.milestone !== null; + return this.activeIssue.milestone !== null; }, groupFullPath() { - const { referencePath = '' } = this.issue; + const { referencePath = '' } = this.activeIssue; return referencePath.slice(0, referencePath.indexOf('/')); }, projectPath() { - const { referencePath = '' } = this.issue; + const { referencePath = '' } = this.activeIssue; return referencePath.slice(0, referencePath.indexOf('#')); }, dropdownText() { - return this.issue.milestone?.title ?? this.$options.i18n.noMilestone; + return this.activeIssue.milestone?.title ?? this.$options.i18n.noMilestone; }, }, mounted() { @@ -120,7 +120,7 @@ export default { @close="edit = false" > <template v-if="hasMilestone" #collapsed> - <strong class="gl-text-gray-900">{{ issue.milestone.title }}</strong> + <strong class="gl-text-gray-900">{{ activeIssue.milestone.title }}</strong> </template> <template> <gl-dropdown @@ -133,7 +133,7 @@ export default { <gl-dropdown-item data-testid="no-milestone-item" :is-check-item="true" - :is-checked="!issue.milestone" + :is-checked="!activeIssue.milestone" @click="setMilestone(null)" > {{ $options.i18n.noMilestone }} @@ -145,7 +145,7 @@ export default { v-for="milestone in milestones" :key="milestone.id" :is-check-item="true" - :is-checked="issue.milestone && milestone.id === issue.milestone.id" + :is-checked="activeIssue.milestone && milestone.id === activeIssue.milestone.id" data-testid="milestone-item" @click="setMilestone(milestone.id)" > diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue index ed069cea630..4aa8d2f55e4 100644 --- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue +++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue @@ -15,7 +15,7 @@ export default { ), }, updateSubscribedErrorMessage: s__( - 'IssueBoards|An error occurred while setting notifications status.', + 'IssueBoards|An error occurred while setting notifications status. Please try again.', ), }, components: { diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue index 4e5a6609042..8d65f3240c8 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue @@ -42,13 +42,13 @@ export default { axios.patch(this.updateUrl, data).catch(() => { Flash(__('Failed to remove issue from board, please try again.')); - lists.forEach(list => { + lists.forEach((list) => { list.addIssue(issue); }); }); // Remove from the frontend store - lists.forEach(list => { + lists.forEach((list) => { list.removeIssue(issue); }); @@ -58,9 +58,11 @@ export default { * Build the default patch request. */ buildPatchRequest(issue, lists) { - const listLabelIds = lists.map(list => list.label.id); + const listLabelIds = lists.map((list) => list.label.id); - const labelIds = issue.labels.map(label => label.id).filter(id => !listLabelIds.includes(id)); + const labelIds = issue.labels + .map((label) => label.id) + .filter((id) => !listLabelIds.includes(id)); return { label_ids: labelIds, diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 1667dcc9f2e..94b35aadaf1 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -23,8 +23,8 @@ export default class FilteredSearchBoards extends FilteredSearchManager { // Issue boards is slightly different, we handle all the requests async // instead or reloading the page, we just re-fire the list ajax requests this.isHandledAsync = true; - this.cantEdit = cantEdit.filter(i => typeof i === 'string'); - this.cantEditWithValue = cantEdit.filter(i => typeof i === 'object'); + this.cantEdit = cantEdit.filter((i) => typeof i === 'string'); + this.cantEditWithValue = cantEdit.filter((i) => typeof i === 'object'); if (vuexstore.getters.shouldUseGraphQL && vuexstore.state.boardConfig) { const boardConfigPath = transformBoardConfig(vuexstore.state.boardConfig); @@ -55,7 +55,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager { const tokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token'); // Remove all the tokens as they will be replaced by the search manager - [].forEach.call(tokens, el => { + [].forEach.call(tokens, (el) => { el.parentNode.removeChild(el); }); @@ -75,7 +75,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager { if (this.cantEdit.includes(tokenName)) return false; return ( this.cantEditWithValue.findIndex( - token => token.name === tokenName && token.value === tokenValue, + (token) => token.name === tokenName && token.value === tokenValue, ) === -1 ); } diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js b/app/assets/javascripts/boards/filters/due_date_filters.js index 9eaa0cd227d..c35dedde71b 100644 --- a/app/assets/javascripts/boards/filters/due_date_filters.js +++ b/app/assets/javascripts/boards/filters/due_date_filters.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import dateFormat from 'dateformat'; -Vue.filter('due-date', value => { +Vue.filter('due-date', (value) => { const date = new Date(value); return dateFormat(date, 'mmm d, yyyy', true); }); diff --git a/app/assets/javascripts/boards/graphql/board.mutation.graphql b/app/assets/javascripts/boards/graphql/board.mutation.graphql deleted file mode 100644 index ef2b81a7939..00000000000 --- a/app/assets/javascripts/boards/graphql/board.mutation.graphql +++ /dev/null @@ -1,11 +0,0 @@ -mutation UpdateBoard($id: ID!, $hideClosedList: Boolean, $hideBacklogList: Boolean) { - updateBoard( - input: { id: $id, hideClosedList: $hideClosedList, hideBacklogList: $hideBacklogList } - ) { - board { - id - hideClosedList - hideBacklogList - } - } -} diff --git a/app/assets/javascripts/boards/graphql/board_create.mutation.graphql b/app/assets/javascripts/boards/graphql/board_create.mutation.graphql new file mode 100644 index 00000000000..b3ea79d6443 --- /dev/null +++ b/app/assets/javascripts/boards/graphql/board_create.mutation.graphql @@ -0,0 +1,9 @@ +mutation createBoard($input: CreateBoardInput!) { + createBoard(input: $input) { + board { + id + webPath + } + errors + } +} diff --git a/app/assets/javascripts/boards/graphql/board_destroy.mutation.graphql b/app/assets/javascripts/boards/graphql/board_destroy.mutation.graphql new file mode 100644 index 00000000000..d4b928749de --- /dev/null +++ b/app/assets/javascripts/boards/graphql/board_destroy.mutation.graphql @@ -0,0 +1,7 @@ +mutation destroyBoard($id: BoardID!) { + destroyBoard(input: { id: $id }) { + board { + id + } + } +} diff --git a/app/assets/javascripts/boards/graphql/board_update.mutation.graphql b/app/assets/javascripts/boards/graphql/board_update.mutation.graphql new file mode 100644 index 00000000000..3abe09079c7 --- /dev/null +++ b/app/assets/javascripts/boards/graphql/board_update.mutation.graphql @@ -0,0 +1,9 @@ +mutation UpdateBoard($input: UpdateBoardInput!) { + updateBoard(input: $input) { + board { + id + webPath + } + errors + } +} diff --git a/app/assets/javascripts/boards/graphql/group_projects.query.graphql b/app/assets/javascripts/boards/graphql/group_projects.query.graphql new file mode 100644 index 00000000000..1afa6e48547 --- /dev/null +++ b/app/assets/javascripts/boards/graphql/group_projects.query.graphql @@ -0,0 +1,17 @@ +#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" + +query getGroupProjects($fullPath: ID!, $search: String, $after: String) { + group(fullPath: $fullPath) { + projects(search: $search, after: $after, first: 100) { + nodes { + id + name + fullPath + nameWithNamespace + } + pageInfo { + ...PageInfo + } + } + } +} diff --git a/app/assets/javascripts/boards/graphql/issue_set_title.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_set_title.mutation.graphql new file mode 100644 index 00000000000..62e6c1352a6 --- /dev/null +++ b/app/assets/javascripts/boards/graphql/issue_set_title.mutation.graphql @@ -0,0 +1,8 @@ +mutation issueSetTitle($input: UpdateIssueInput!) { + updateIssue(input: $input) { + issue { + title + } + errors + } +} diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 64a4f246735..ef70a094f7c 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -55,7 +55,7 @@ export default () => { const $boardApp = document.getElementById('board-app'); // check for browser back and trigger a hard reload to circumvent browser caching. - window.addEventListener('pageshow', event => { + window.addEventListener('pageshow', (event) => { const isNavTypeBackForward = window.performance && window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD; @@ -68,8 +68,10 @@ export default () => { issueBoardsApp.$destroy(true); } - boardsStore.create(); - boardsStore.setTimeTrackingLimitToHours($boardApp.dataset.timeTrackingLimitToHours); + if (!gon?.features?.graphqlBoardLists) { + boardsStore.create(); + boardsStore.setTimeTrackingLimitToHours($boardApp.dataset.timeTrackingLimitToHours); + } issueBoardsApp = new Vue({ el: $boardApp, @@ -117,16 +119,9 @@ export default () => { }, }, created() { - const endpoints = { - boardsEndpoint: this.boardsEndpoint, - recentBoardsEndpoint: this.recentBoardsEndpoint, - listsEndpoint: this.listsEndpoint, - bulkUpdatePath: this.bulkUpdatePath, + this.setInitialBoardData({ boardId: $boardApp.dataset.boardId, fullPath: $boardApp.dataset.fullPath, - }; - this.setInitialBoardData({ - ...endpoints, boardType: this.parent, disabled: this.disabled, boardConfig: { @@ -134,14 +129,23 @@ export default () => { milestoneTitle: $boardApp.dataset.boardMilestoneTitle || '', iterationId: parseInt($boardApp.dataset.boardIterationId, 10), iterationTitle: $boardApp.dataset.boardIterationTitle || '', + assigneeId: $boardApp.dataset.boardAssigneeId, assigneeUsername: $boardApp.dataset.boardAssigneeUsername, - labels: $boardApp.dataset.labels ? JSON.parse($boardApp.dataset.labels || []) : [], + labels: $boardApp.dataset.labels ? JSON.parse($boardApp.dataset.labels) : [], + labelIds: $boardApp.dataset.labelIds ? JSON.parse($boardApp.dataset.labelIds) : [], weight: $boardApp.dataset.boardWeight ? parseInt($boardApp.dataset.boardWeight, 10) : null, }, }); - boardsStore.setEndpoints(endpoints); + boardsStore.setEndpoints({ + boardsEndpoint: this.boardsEndpoint, + recentBoardsEndpoint: this.recentBoardsEndpoint, + listsEndpoint: this.listsEndpoint, + bulkUpdatePath: this.bulkUpdatePath, + boardId: $boardApp.dataset.boardId, + fullPath: $boardApp.dataset.fullPath, + }); boardsStore.rootPath = this.boardsEndpoint; eventHub.$on('updateTokens', this.updateTokens); @@ -174,9 +178,9 @@ export default () => { initialBoardLoad() { boardsStore .all() - .then(res => res.data) - .then(lists => { - lists.forEach(list => boardsStore.addList(list)); + .then((res) => res.data) + .then((lists) => { + lists.forEach((list) => boardsStore.addList(list)); this.loading = false; }) .catch(() => { @@ -194,8 +198,8 @@ export default () => { setEpicFetchingState(newIssue, true); boardsStore .getIssueInfo(sidebarInfoEndpoint) - .then(res => res.data) - .then(data => { + .then((res) => res.data) + .then((data) => { const { subscribed, totalTimeSpent, @@ -305,7 +309,7 @@ export default () => { if (!this.store) { return true; } - return !this.store.lists.filter(list => !list.preset).length; + return !this.store.lists.filter((list) => !list.preset).length; }, }, methods: { @@ -335,7 +339,7 @@ export default () => { } mountMultipleBoardsSwitcher({ - boardsEndpoint: $boardApp.dataset.boardsEndpoint, - recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint, + fullPath: $boardApp.dataset.fullPath, + rootPath: $boardApp.dataset.boardsEndpoint, }); }; diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js index f02c92e4230..a95d749d71c 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js @@ -24,7 +24,7 @@ export function getBoardSortableDefaultOptions(obj) { onEnd: sortableEnd, }; - Object.keys(obj).forEach(key => { + Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; }); return defaultSortOptions; diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index 822e6d62ab3..1e77326ba9c 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -70,7 +70,7 @@ class ListIssue { } getLists() { - return boardsStore.state.lists.filter(list => list.findIssue(this.id)); + return boardsStore.state.lists.filter((list) => list.findIssue(this.id)); } updateData(newData) { diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index 09f5d5b4dd8..be02ac7b889 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -35,7 +35,7 @@ class List { constructor(obj) { this.id = obj.id; this.position = obj.position; - this.title = (obj.list_type || obj.listType) === 'backlog' ? __('Open') : obj.title; + this.title = obj.title; this.type = obj.list_type || obj.listType; const typeInfo = this.getTypeInfo(this.type); @@ -134,7 +134,7 @@ class List { updateMultipleIssues(issues, listFrom, moveBeforeId, moveAfterId) { boardsStore .moveMultipleIssues({ - ids: issues.map(issue => issue.id), + ids: issues.map((issue) => issue.id), fromListId: listFrom.id, toListId: this.id, moveBeforeId, diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js index df65ebb7526..738c8fb927e 100644 --- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js +++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js @@ -10,7 +10,7 @@ const apolloProvider = new VueApollo({ defaultClient: createDefaultClient(), }); -export default (endpoints = {}) => { +export default (params = {}) => { const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher'); return new Vue({ el: boardsSwitcherElement, @@ -18,6 +18,10 @@ export default (endpoints = {}) => { BoardsSelector, }, apolloProvider, + provide: { + fullPath: params.fullPath, + rootPath: params.rootPath, + }, data() { const { dataset } = boardsSwitcherElement; @@ -35,9 +39,6 @@ export default (endpoints = {}) => { return { boardsSelectorProps }; }, - provide: { - endpoints, - }, render(createElement) { return createElement(BoardsSelector, { props: this.boardsSelectorProps, diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index 59b97eba9fe..1d34f21798a 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -12,6 +12,8 @@ import { fullBoardId, formatListsPageInfo, formatIssue, + formatIssueInput, + updateListPosition, } from '../boards_util'; import createFlash from '~/flash'; import { __ } from '~/locale'; @@ -27,6 +29,8 @@ import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql import issueSetDueDateMutation from '../graphql/issue_set_due_date.mutation.graphql'; import issueSetSubscriptionMutation from '../graphql/issue_set_subscription.mutation.graphql'; import issueSetMilestoneMutation from '../graphql/issue_set_milestone.mutation.graphql'; +import issueSetTitleMutation from '../graphql/issue_set_title.mutation.graphql'; +import groupProjectsQuery from '../graphql/group_projects.query.graphql'; const notImplemented = () => { /* eslint-disable-next-line @gitlab/require-i18n-strings */ @@ -78,8 +82,7 @@ export default { }, fetchLists: ({ commit, state, dispatch }) => { - const { endpoints, boardType, filterParams } = state; - const { fullPath, boardId } = endpoints; + const { boardType, filterParams, fullPath, boardId } = state; const variables = { fullPath, @@ -98,7 +101,7 @@ export default { const { lists, hideBacklogList } = data[boardType]?.board; commit(types.RECEIVE_BOARD_LISTS_SUCCESS, formatBoardLists(lists)); // Backlog list needs to be created if it doesn't exist and it's not hidden - if (!lists.nodes.find(l => l.listType === ListType.backlog) && !hideBacklogList) { + if (!lists.nodes.find((l) => l.listType === ListType.backlog) && !hideBacklogList) { dispatch('createList', { backlog: true }); } }) @@ -106,7 +109,7 @@ export default { }, createList: ({ state, commit, dispatch }, { backlog, labelId, milestoneId, assigneeId }) => { - const { boardId } = state.endpoints; + const { boardId } = state; gqlClient .mutate({ @@ -131,12 +134,11 @@ export default { }, addList: ({ commit }, list) => { - commit(types.RECEIVE_ADD_LIST_SUCCESS, list); + commit(types.RECEIVE_ADD_LIST_SUCCESS, updateListPosition(list)); }, fetchLabels: ({ state, commit }, searchTerm) => { - const { endpoints, boardType } = state; - const { fullPath } = endpoints; + const { fullPath, boardType } = state; const variables = { fullPath, @@ -214,11 +216,17 @@ export default { listId, }, }) - .then(({ data: { destroyBoardList: { errors } } }) => { - if (errors.length > 0) { - commit(types.REMOVE_LIST_FAILURE, listsBackup); - } - }) + .then( + ({ + data: { + destroyBoardList: { errors }, + }, + }) => { + if (errors.length > 0) { + commit(types.REMOVE_LIST_FAILURE, listsBackup); + } + }, + ) .catch(() => { commit(types.REMOVE_LIST_FAILURE, listsBackup); }); @@ -227,8 +235,7 @@ export default { fetchIssuesForList: ({ state, commit }, { listId, fetchNext = false }) => { commit(types.REQUEST_ISSUES_FOR_LIST, { listId, fetchNext }); - const { endpoints, boardType, filterParams } = state; - const { fullPath, boardId } = endpoints; + const { fullPath, boardId, boardType, filterParams } = state; const variables = { fullPath, @@ -271,7 +278,7 @@ export default { const originalIndex = fromList.indexOf(Number(issueId)); commit(types.MOVE_ISSUE, { originalIssue, fromListId, toListId, moveBeforeId, moveAfterId }); - const { boardId } = state.endpoints; + const { boardId } = state; const [fullProjectPath] = issuePath.split(/[#]/); gqlClient @@ -356,10 +363,13 @@ export default { }, createNewIssue: ({ commit, state }, issueInput) => { - const input = issueInput; - const { boardType, endpoints } = state; + const { boardConfig } = state; + + const input = formatIssueInput(issueInput, boardConfig); + + const { boardType, fullPath } = state; if (boardType === BoardType.project) { - input.projectPath = endpoints.fullPath; + input.projectPath = fullPath; } return gqlClient @@ -387,7 +397,7 @@ export default { commit(types.ADD_ISSUE_TO_LIST, { list, issue, position: 0 }); dispatch('createNewIssue', issueInput) - .then(res => { + .then((res) => { commit(types.ADD_ISSUE_TO_LIST, { list, issue: formatIssue({ ...res, id: getIdFromGraphQLId(res.id) }), @@ -469,6 +479,61 @@ export default { }); }, + setActiveIssueTitle: async ({ commit, getters }, input) => { + const { activeIssue } = getters; + const { data } = await gqlClient.mutate({ + mutation: issueSetTitleMutation, + variables: { + input: { + iid: String(activeIssue.iid), + projectPath: input.projectPath, + title: input.title, + }, + }, + }); + + if (data.updateIssue?.errors?.length > 0) { + throw new Error(data.updateIssue.errors); + } + + commit(types.UPDATE_ISSUE_BY_ID, { + issueId: activeIssue.id, + prop: 'title', + value: data.updateIssue.issue.title, + }); + }, + + fetchGroupProjects: ({ commit, state }, { search = '', fetchNext = false }) => { + commit(types.REQUEST_GROUP_PROJECTS, fetchNext); + + const { fullPath } = state; + + const variables = { + fullPath, + search: search !== '' ? search : undefined, + after: fetchNext ? state.groupProjectsFlags.pageInfo.endCursor : undefined, + }; + + return gqlClient + .query({ + query: groupProjectsQuery, + variables, + }) + .then(({ data }) => { + const { projects } = data.group; + commit(types.RECEIVE_GROUP_PROJECTS_SUCCESS, { + projects: projects.nodes, + pageInfo: projects.pageInfo, + fetchNext, + }); + }) + .catch(() => commit(types.RECEIVE_GROUP_PROJECTS_FAILURE)); + }, + + setSelectedProject: ({ commit }, project) => { + commit(types.SET_SELECTED_PROJECT, project); + }, + fetchBacklog: () => { notImplemented(); }, diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 36702b6ca5f..f59530ddf8f 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -101,7 +101,7 @@ const boardsStore = { }, new(listObj) { const list = this.addList(listObj); - const backlogList = this.findList('type', 'backlog', 'backlog'); + const backlogList = this.findList('type', 'backlog'); list .save() @@ -124,7 +124,7 @@ const boardsStore = { }, findIssueLabel(issue, findLabel) { - return issue.labels.find(label => label.id === findLabel.id); + return issue.labels.find((label) => label.id === findLabel.id); }, goToNextPage(list) { @@ -182,15 +182,15 @@ const boardsStore = { } }, findListIssue(list, id) { - return list.issues.find(issue => issue.id === id); + return list.issues.find((issue) => issue.id === id); }, - removeList(id, type = 'blank') { - const list = this.findList('id', id, type); + removeList(id) { + const list = this.findList('id', id); if (!list) return; - this.state.lists = this.state.lists.filter(list => list.id !== id); + this.state.lists = this.state.lists.filter((list) => list.id !== id); }, moveList(listFrom, orderLists) { orderLists.forEach((id, i) => { @@ -205,7 +205,7 @@ const boardsStore = { let moveBeforeId = null; let moveAfterId = null; - const listHasIssues = issues.every(issue => list.findIssue(issue.id)); + const listHasIssues = issues.every((issue) => list.findIssue(issue.id)); if (!listHasIssues) { if (newIndex !== undefined) { @@ -223,21 +223,21 @@ const boardsStore = { } if (list.label) { - issues.forEach(issue => issue.addLabel(list.label)); + issues.forEach((issue) => issue.addLabel(list.label)); } if (list.assignee) { if (listFrom && listFrom.type === 'assignee') { - issues.forEach(issue => issue.removeAssignee(listFrom.assignee)); + issues.forEach((issue) => issue.removeAssignee(listFrom.assignee)); } - issues.forEach(issue => issue.addAssignee(list.assignee)); + issues.forEach((issue) => issue.addAssignee(list.assignee)); } if (IS_EE && list.milestone) { if (listFrom && listFrom.type === 'milestone') { - issues.forEach(issue => issue.removeMilestone(listFrom.milestone)); + issues.forEach((issue) => issue.removeMilestone(listFrom.milestone)); } - issues.forEach(issue => issue.addMilestone(list.milestone)); + issues.forEach((issue) => issue.addMilestone(list.milestone)); } if (listFrom) { @@ -249,7 +249,7 @@ const boardsStore = { }, removeListIssues(list, removeIssue) { - list.issues = list.issues.filter(issue => { + list.issues = list.issues.filter((issue) => { const matchesRemove = removeIssue.id === issue.id; if (matchesRemove) { @@ -261,9 +261,9 @@ const boardsStore = { }); }, removeListMultipleIssues(list, removeIssues) { - const ids = removeIssues.map(issue => issue.id); + const ids = removeIssues.map((issue) => issue.id); - list.issues = list.issues.filter(issue => { + list.issues = list.issues.filter((issue) => { const matchesRemove = ids.includes(issue.id); if (matchesRemove) { @@ -289,9 +289,9 @@ const boardsStore = { }, moveMultipleIssuesToList({ listFrom, listTo, issues, newIndex }) { - const issueTo = issues.map(issue => listTo.findIssue(issue.id)); - const issueLists = issues.map(issue => issue.getLists()).flat(); - const listLabels = issueLists.map(list => list.label); + const issueTo = issues.map((issue) => listTo.findIssue(issue.id)); + const issueLists = issues.map((issue) => issue.getLists()).flat(); + const listLabels = issueLists.map((list) => list.label); const hasMoveableIssues = issueTo.filter(Boolean).length > 0; if (!hasMoveableIssues) { @@ -299,30 +299,30 @@ const boardsStore = { if ( listTo.type === ListType.assignee && listFrom.type === ListType.assignee && - issues.some(issue => issue.findAssignee(listTo.assignee)) + issues.some((issue) => issue.findAssignee(listTo.assignee)) ) { - const targetIssues = issues.map(issue => listTo.findIssue(issue.id)); - targetIssues.forEach(targetIssue => targetIssue.removeAssignee(listFrom.assignee)); + const targetIssues = issues.map((issue) => listTo.findIssue(issue.id)); + targetIssues.forEach((targetIssue) => targetIssue.removeAssignee(listFrom.assignee)); } else if (listTo.type === 'milestone') { - const currentMilestones = issues.map(issue => issue.milestone); + const currentMilestones = issues.map((issue) => issue.milestone); const currentLists = this.state.lists - .filter(list => list.type === 'milestone' && list.id !== listTo.id) - .filter(list => - list.issues.some(listIssue => issues.some(issue => listIssue.id === issue.id)), + .filter((list) => list.type === 'milestone' && list.id !== listTo.id) + .filter((list) => + list.issues.some((listIssue) => issues.some((issue) => listIssue.id === issue.id)), ); - issues.forEach(issue => { - currentMilestones.forEach(milestone => { + issues.forEach((issue) => { + currentMilestones.forEach((milestone) => { issue.removeMilestone(milestone); }); }); - issues.forEach(issue => { + issues.forEach((issue) => { issue.addMilestone(listTo.milestone); }); - currentLists.forEach(currentList => { - issues.forEach(issue => { + currentLists.forEach((currentList) => { + issues.forEach((issue) => { currentList.removeIssue(issue); }); }); @@ -334,36 +334,36 @@ const boardsStore = { } } else { listTo.updateMultipleIssues(issues, listFrom); - issues.forEach(issue => { + issues.forEach((issue) => { issue.removeLabel(listFrom.label); }); } if (listTo.type === ListType.closed && listFrom.type !== ListType.backlog) { - issueLists.forEach(list => { - issues.forEach(issue => { + issueLists.forEach((list) => { + issues.forEach((issue) => { list.removeIssue(issue); }); }); - issues.forEach(issue => { + issues.forEach((issue) => { issue.removeLabels(listLabels); }); } else if (listTo.type === ListType.backlog && listFrom.type === ListType.assignee) { - issues.forEach(issue => { + issues.forEach((issue) => { issue.removeAssignee(listFrom.assignee); }); - issueLists.forEach(list => { - issues.forEach(issue => { + issueLists.forEach((list) => { + issues.forEach((issue) => { list.removeIssue(issue); }); }); } else if (listTo.type === ListType.backlog && listFrom.type === ListType.milestone) { - issues.forEach(issue => { + issues.forEach((issue) => { issue.removeMilestone(listFrom.milestone); }); - issueLists.forEach(list => { - issues.forEach(issue => { + issueLists.forEach((list) => { + issues.forEach((issue) => { list.removeIssue(issue); }); }); @@ -380,8 +380,8 @@ const boardsStore = { if (issues.length === 1) return true; // Create list of ids for issues involved. - const listIssueIds = list.issues.map(issue => issue.id); - const movedIssueIds = issues.map(issue => issue.id); + const listIssueIds = list.issues.map((issue) => issue.id); + const movedIssueIds = issues.map((issue) => issue.id); // Check if moved issue IDs is sub-array // of source list issue IDs (i.e. contiguous selection). @@ -391,7 +391,7 @@ const boardsStore = { moveIssueToList(listFrom, listTo, issue, newIndex) { const issueTo = listTo.findIssue(issue.id); const issueLists = issue.getLists(); - const listLabels = issueLists.map(listIssue => listIssue.label); + const listLabels = issueLists.map((listIssue) => listIssue.label); if (!issueTo) { // Check if target list assignee is already present in this issue @@ -405,12 +405,12 @@ const boardsStore = { } else if (listTo.type === 'milestone') { const currentMilestone = issue.milestone; const currentLists = this.state.lists - .filter(list => list.type === 'milestone' && list.id !== listTo.id) - .filter(list => list.issues.some(listIssue => issue.id === listIssue.id)); + .filter((list) => list.type === 'milestone' && list.id !== listTo.id) + .filter((list) => list.issues.some((listIssue) => issue.id === listIssue.id)); issue.removeMilestone(currentMilestone); issue.addMilestone(listTo.milestone); - currentLists.forEach(currentList => currentList.removeIssue(issue)); + currentLists.forEach((currentList) => currentList.removeIssue(issue)); listTo.addIssue(issue, listFrom, newIndex); } else { // Add to new lists issues if it doesn't already exist @@ -422,7 +422,7 @@ const boardsStore = { } if (listTo.type === 'closed' && listFrom.type !== 'backlog') { - issueLists.forEach(list => { + issueLists.forEach((list) => { list.removeIssue(issue); }); issue.removeLabels(listLabels); @@ -461,18 +461,11 @@ const boardsStore = { moveAfterId: afterId, }); }, - findList(key, val, type = 'label') { - const filteredList = this.state.lists.filter(list => { - const byType = type - ? list.type === type || list.type === 'assignee' || list.type === 'milestone' - : true; - - return list[key] === val && byType; - }); - return filteredList[0]; + findList(key, val) { + return this.state.lists.find((list) => list[key] === val); }, findListByLabelId(id) { - return this.state.lists.find(list => list.type === 'label' && list.label.id === id); + return this.state.lists.find((list) => list.type === 'label' && list.label.id === id); }, toggleFilter(filter) { @@ -589,8 +582,8 @@ const boardsStore = { } return this.createList(entity.id, entityType) - .then(res => res.data) - .then(data => { + .then((res) => res.data) + .then((data) => { list.id = data.id; list.type = data.list_type; list.position = data.position; @@ -607,7 +600,7 @@ const boardsStore = { }; if (list.label && data.label_name) { - data.label_name = data.label_name.filter(label => label !== list.label.title); + data.label_name = data.label_name.filter((label) => label !== list.label.title); } if (emptyIssues) { @@ -615,8 +608,8 @@ const boardsStore = { } return this.getIssuesForList(list.id, data) - .then(res => res.data) - .then(data => { + .then((res) => res.data) + .then((data) => { list.loading = false; list.issuesSize = data.size; @@ -624,7 +617,7 @@ const boardsStore = { list.issues = []; } - data.issues.forEach(issueObj => { + data.issues.forEach((issueObj) => { list.addIssue(new ListIssue(issueObj)); }); @@ -634,7 +627,7 @@ const boardsStore = { getIssuesForList(id, filter = {}) { const data = { id }; - Object.keys(filter).forEach(key => { + Object.keys(filter).forEach((key) => { data[key] = filter[key]; }); @@ -670,13 +663,13 @@ const boardsStore = { }, moveListMultipleIssues({ list, issues, oldIndicies, newIndex, moveBeforeId, moveAfterId }) { - oldIndicies.reverse().forEach(index => { + oldIndicies.reverse().forEach((index) => { list.issues.splice(index, 1); }); list.issues.splice(newIndex, 0, ...issues); return this.moveMultipleIssues({ - ids: issues.map(issue => issue.id), + ids: issues.map((issue) => issue.id), fromListId: null, toListId: null, moveBeforeId, @@ -703,8 +696,8 @@ const boardsStore = { } return this.newIssue(list.id, issue) - .then(res => res.data) - .then(data => list.onNewIssueResponse(issue, data)); + .then((res) => res.data) + .then((data) => list.onNewIssueResponse(issue, data)); }, getBacklog(data) { @@ -717,7 +710,7 @@ const boardsStore = { }, removeIssueLabel(issue, removeLabel) { if (removeLabel) { - issue.labels = issue.labels.filter(label => removeLabel.id !== label.id); + issue.labels = issue.labels.filter((label) => removeLabel.id !== label.id); } }, @@ -753,16 +746,12 @@ const boardsStore = { return axios.get(this.state.endpoints.recentBoardsEndpoint); }, - deleteBoard({ id }) { - return axios.delete(this.generateBoardsPath(id)); - }, - setCurrentBoard(board) { this.state.currentBoard = board; }, toggleMultiSelect(issue) { - const selectedIssueIds = this.multiSelect.list.map(issue => issue.id); + const selectedIssueIds = this.multiSelect.list.map((issue) => issue.id); const index = selectedIssueIds.indexOf(issue.id); if (index === -1) { @@ -777,12 +766,12 @@ const boardsStore = { }, removeIssueAssignee(issue, removeAssignee) { if (removeAssignee) { - issue.assignees = issue.assignees.filter(assignee => assignee.id !== removeAssignee.id); + issue.assignees = issue.assignees.filter((assignee) => assignee.id !== removeAssignee.id); } }, findIssueAssignee(issue, findAssignee) { - return issue.assignees.find(assignee => assignee.id === findAssignee.id); + return issue.assignees.find((assignee) => assignee.id === findAssignee.id); }, clearMultiSelect() { @@ -837,11 +826,11 @@ const boardsStore = { } if (obj.labels) { - issue.labels = obj.labels.map(label => new ListLabel(label)); + issue.labels = obj.labels.map((label) => new ListLabel(label)); } if (obj.assignees) { - issue.assignees = obj.assignees.map(a => new ListAssignee(a)); + issue.assignees = obj.assignees.map((a) => new ListAssignee(a)); } }, addIssueLabel(issue, label) { diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js index ca6887b6f45..d72b5c6fb8e 100644 --- a/app/assets/javascripts/boards/stores/getters.js +++ b/app/assets/javascripts/boards/stores/getters.js @@ -2,18 +2,18 @@ import { find } from 'lodash'; import { inactiveId } from '../constants'; export default { - isSidebarOpen: state => state.activeId !== inactiveId, + isSidebarOpen: (state) => state.activeId !== inactiveId, isSwimlanesOn: () => false, - getIssueById: state => id => { + getIssueById: (state) => (id) => { return state.issues[id] || {}; }, - getIssuesByList: (state, getters) => listId => { + getIssuesByList: (state, getters) => (listId) => { const listIssueIds = state.issuesByListId[listId] || []; - return listIssueIds.map(id => getters.getIssueById(id)); + return listIssueIds.map((id) => getters.getIssueById(id)); }, - activeIssue: state => { + activeIssue: (state) => { return state.issues[state.activeId] || {}; }, @@ -22,12 +22,12 @@ export default { return referencePath.slice(0, referencePath.indexOf('#')); }, - getListByLabelId: state => labelId => { - return find(state.boardLists, l => l.label?.id === labelId); + getListByLabelId: (state) => (labelId) => { + return find(state.boardLists, (l) => l.label?.id === labelId); }, - getListByTitle: state => title => { - return find(state.boardLists, l => l.title === title); + getListByTitle: (state) => (title) => { + return find(state.boardLists, (l) => l.title === title); }, shouldUseGraphQL: () => { diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js index b7228bf7bf5..8a8fa61361c 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js +++ b/app/assets/javascripts/boards/stores/modal_store.js @@ -40,7 +40,7 @@ class ModalStore { toggleAll() { const select = this.selectedCount() !== this.store.issues.length; - this.store.issues.forEach(issue => { + this.store.issues.forEach((issue) => { const issueUpdate = issue; if (issueUpdate.selected !== select) { @@ -56,7 +56,7 @@ class ModalStore { } getSelectedIssues() { - return this.store.selectedIssues.filter(issue => issue.selected); + return this.store.selectedIssues.filter((issue) => issue.selected); } addSelectedIssue(issue) { @@ -70,13 +70,13 @@ class ModalStore { removeSelectedIssue(issue, forcePurge = false) { if (this.store.activeTab === 'all' || forcePurge) { this.store.selectedIssues = this.store.selectedIssues.filter( - fIssue => fIssue.id !== issue.id, + (fIssue) => fIssue.id !== issue.id, ); } } purgeUnselectedIssues() { - this.store.selectedIssues.forEach(issue => { + this.store.selectedIssues.forEach((issue) => { if (!issue.selected) { this.removeSelectedIssue(issue, true); } @@ -88,7 +88,7 @@ class ModalStore { } findSelectedIssue(issue) { - return this.store.selectedIssues.filter(filteredIssue => filteredIssue.id === issue.id)[0]; + return this.store.selectedIssues.filter((filteredIssue) => filteredIssue.id === issue.id)[0]; } } diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js index 2b2c2bee51c..4697f39498a 100644 --- a/app/assets/javascripts/boards/stores/mutation_types.js +++ b/app/assets/javascripts/boards/stores/mutation_types.js @@ -36,3 +36,7 @@ export const SET_ACTIVE_ID = 'SET_ACTIVE_ID'; export const UPDATE_ISSUE_BY_ID = 'UPDATE_ISSUE_BY_ID'; export const SET_ASSIGNEE_LOADING = 'SET_ASSIGNEE_LOADING'; export const RESET_ISSUES = 'RESET_ISSUES'; +export const REQUEST_GROUP_PROJECTS = 'REQUEST_GROUP_PROJECTS'; +export const RECEIVE_GROUP_PROJECTS_SUCCESS = 'RECEIVE_GROUP_PROJECTS_SUCCESS'; +export const RECEIVE_GROUP_PROJECTS_FAILURE = 'RECEIVE_GROUP_PROJECTS_FAILURE'; +export const SET_SELECTED_PROJECT = 'SET_SELECTED_PROJECT'; diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js index 8c4e514710f..6c79b22d308 100644 --- a/app/assets/javascripts/boards/stores/mutations.js +++ b/app/assets/javascripts/boards/stores/mutations.js @@ -32,8 +32,9 @@ export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfter export default { [mutationTypes.SET_INITIAL_BOARD_DATA](state, data) { - const { boardType, disabled, boardConfig, ...endpoints } = data; - state.endpoints = endpoints; + const { boardType, disabled, boardId, fullPath, boardConfig } = data; + state.boardId = boardId; + state.fullPath = fullPath; state.boardType = boardType; state.disabled = disabled; state.boardConfig = boardConfig; @@ -43,7 +44,7 @@ export default { state.boardLists = lists; }, - [mutationTypes.RECEIVE_BOARD_LISTS_FAILURE]: state => { + [mutationTypes.RECEIVE_BOARD_LISTS_FAILURE]: (state) => { state.error = s__( 'Boards|An error occurred while fetching the board lists. Please reload the page.', ); @@ -58,15 +59,15 @@ export default { state.filterParams = filterParams; }, - [mutationTypes.CREATE_LIST_FAILURE]: state => { + [mutationTypes.CREATE_LIST_FAILURE]: (state) => { state.error = s__('Boards|An error occurred while creating the list. Please try again.'); }, - [mutationTypes.RECEIVE_LABELS_FAILURE]: state => { + [mutationTypes.RECEIVE_LABELS_FAILURE]: (state) => { state.error = s__('Boards|An error occurred while fetching labels. Please reload the page.'); }, - [mutationTypes.GENERATE_DEFAULT_LISTS_FAILURE]: state => { + [mutationTypes.GENERATE_DEFAULT_LISTS_FAILURE]: (state) => { state.error = s__('Boards|An error occurred while generating lists. Please reload the page.'); }, @@ -128,8 +129,8 @@ export default { Vue.set(state.listsFlags, listId, { isLoading: false, isLoadingMore: false }); }, - [mutationTypes.RESET_ISSUES]: state => { - Object.keys(state.issuesByListId).forEach(listId => { + [mutationTypes.RESET_ISSUES]: (state) => { + Object.keys(state.issuesByListId).forEach((listId) => { Vue.set(state.issuesByListId, listId, []); }); }, @@ -205,7 +206,7 @@ export default { notImplemented(); }, - [mutationTypes.CREATE_ISSUE_FAILURE]: state => { + [mutationTypes.CREATE_ISSUE_FAILURE]: (state) => { state.error = s__('Boards|An error occurred while creating the issue. Please try again.'); }, @@ -236,4 +237,25 @@ export default { [mutationTypes.TOGGLE_EMPTY_STATE]: () => { notImplemented(); }, + + [mutationTypes.REQUEST_GROUP_PROJECTS]: (state, fetchNext) => { + Vue.set(state, 'groupProjectsFlags', { + [fetchNext ? 'isLoadingMore' : 'isLoading']: true, + pageInfo: state.groupProjectsFlags.pageInfo, + }); + }, + + [mutationTypes.RECEIVE_GROUP_PROJECTS_SUCCESS]: (state, { projects, pageInfo, fetchNext }) => { + Vue.set(state, 'groupProjects', fetchNext ? [...state.groupProjects, ...projects] : projects); + Vue.set(state, 'groupProjectsFlags', { isLoading: false, isLoadingMore: false, pageInfo }); + }, + + [mutationTypes.RECEIVE_GROUP_PROJECTS_FAILURE]: (state) => { + state.error = s__('Boards|An error occurred while fetching group projects. Please try again.'); + Vue.set(state, 'groupProjectsFlags', { isLoading: false, isLoadingMore: false }); + }, + + [mutationTypes.SET_SELECTED_PROJECT]: (state, project) => { + state.selectedProject = project; + }, }; diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js index 573e98e56e0..aba7da373cf 100644 --- a/app/assets/javascripts/boards/stores/state.js +++ b/app/assets/javascripts/boards/stores/state.js @@ -1,7 +1,6 @@ import { inactiveId } from '~/boards/constants'; export default () => ({ - endpoints: {}, boardType: null, disabled: false, isShowingLabels: true, @@ -15,6 +14,13 @@ export default () => ({ issues: {}, filterParams: {}, boardConfig: {}, + groupProjects: [], + groupProjectsFlags: { + isLoading: false, + isLoadingMore: false, + pageInfo: {}, + }, + selectedProject: {}, error: undefined, // TODO: remove after ce/ee split of board_content.vue isShowingEpicsSwimlanes: false, diff --git a/app/assets/javascripts/branches/divergence_graph.js b/app/assets/javascripts/branches/divergence_graph.js index 89e9d3fcb62..a577bdca082 100644 --- a/app/assets/javascripts/branches/divergence_graph.js +++ b/app/assets/javascripts/branches/divergence_graph.js @@ -21,7 +21,7 @@ export function createGraphVueApp(el, data, maxCommits) { }); } -export default endpoint => { +export default (endpoint) => { const names = [...document.querySelectorAll('.js-branch-item')].map( ({ dataset }) => dataset.name, ); diff --git a/app/assets/javascripts/breadcrumb.js b/app/assets/javascripts/breadcrumb.js index ff7f734f998..b8b8a0b2867 100644 --- a/app/assets/javascripts/breadcrumb.js +++ b/app/assets/javascripts/breadcrumb.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import { hide } from '~/tooltips'; -export const addTooltipToEl = el => { +export const addTooltipToEl = (el) => { const textEl = el.querySelector('.js-breadcrumb-item-text'); if (textEl && textEl.scrollWidth > textEl.offsetWidth) { @@ -16,14 +16,14 @@ export default () => { if (breadcrumbs) { const topLevelLinks = [...breadcrumbs.children] - .filter(el => !el.classList.contains('dropdown')) - .map(el => el.querySelector('a')) - .filter(el => el); + .filter((el) => !el.classList.contains('dropdown')) + .map((el) => el.querySelector('a')) + .filter((el) => el); const $expander = $('.js-breadcrumbs-collapsed-expander'); - topLevelLinks.forEach(el => addTooltipToEl(el)); + topLevelLinks.forEach((el) => addTooltipToEl(el)); - $expander.closest('.dropdown').on('show.bs.dropdown hide.bs.dropdown', e => { + $expander.closest('.dropdown').on('show.bs.dropdown hide.bs.dropdown', (e) => { const $el = $('.js-breadcrumbs-collapsed-expander', e.currentTarget); $el.toggleClass('open'); diff --git a/app/assets/javascripts/broadcast_notification.js b/app/assets/javascripts/broadcast_notification.js index 97da6fa34da..2cf2e922f68 100644 --- a/app/assets/javascripts/broadcast_notification.js +++ b/app/assets/javascripts/broadcast_notification.js @@ -15,5 +15,5 @@ const handleOnDismiss = ({ currentTarget }) => { export default () => { document .querySelectorAll('.js-dismiss-current-broadcast-notification') - .forEach(dismissButton => dismissButton.addEventListener('click', handleOnDismiss)); + .forEach((dismissButton) => dismissButton.addEventListener('click', handleOnDismiss)); }; diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js index 8324c649538..42e0f8b37bd 100644 --- a/app/assets/javascripts/build_artifacts.js +++ b/app/assets/javascripts/build_artifacts.js @@ -14,17 +14,17 @@ export default class BuildArtifacts { // eslint-disable-next-line class-methods-use-this disablePropagation() { - $('.top-block').on('click', '.download', e => { + $('.top-block').on('click', '.download', (e) => { e.stopPropagation(); }); - return $('.tree-holder').on('click', 'tr[data-link] a', e => { + return $('.tree-holder').on('click', 'tr[data-link] a', (e) => { e.stopImmediatePropagation(); }); } // eslint-disable-next-line class-methods-use-this setupEntryClick() { - return $('.tree-holder').on('click', 'tr[data-link]', function() { + return $('.tree-holder').on('click', 'tr[data-link]', function () { visitUrl(this.dataset.link, parseBoolean(this.dataset.externalLink)); }); } @@ -41,12 +41,12 @@ export default class BuildArtifacts { // We want the tooltip to show if you hover anywhere on the row // But be placed below and in the middle of the file name $('.js-artifact-tree-row') - .on('mouseenter', e => { + .on('mouseenter', (e) => { const $el = $(e.currentTarget).find('.js-artifact-tree-tooltip'); show($el); }) - .on('mouseleave', e => { + .on('mouseleave', (e) => { const $el = $(e.currentTarget).find('.js-artifact-tree-tooltip'); hide($el); diff --git a/app/assets/javascripts/ci_lint/components/ci_lint.vue b/app/assets/javascripts/ci_lint/components/ci_lint.vue index 731ed2ddd01..fc47fe8c333 100644 --- a/app/assets/javascripts/ci_lint/components/ci_lint.vue +++ b/app/assets/javascripts/ci_lint/components/ci_lint.vue @@ -111,7 +111,7 @@ export default { <gl-form-checkbox v-model="dryRun" >{{ __('Simulate a pipeline created for the default branch') }} <gl-link :href="pipelineSimulationHelpPagePath" target="_blank" - ><gl-icon class="gl-text-blue-600" name="question-o"/></gl-link + ><gl-icon class="gl-text-blue-600" name="question-o" /></gl-link ></gl-form-checkbox> </div> <gl-button data-testid="ci-lint-clear" @click="clear">{{ __('Clear') }}</gl-button> diff --git a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue index ad07052a298..b84c188cd08 100644 --- a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue +++ b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue @@ -61,7 +61,7 @@ export default { class="triggers-list" responsive > - <template #cell(token)="{item}"> + <template #cell(token)="{ item }"> {{ item.token }} <clipboard-button v-if="item.hasTokenExposed" @@ -82,7 +82,7 @@ export default { </gl-badge> </div> </template> - <template #cell(description)="{item}"> + <template #cell(description)="{ item }"> <tooltip-on-truncate :title="item.description" truncate-target="child" @@ -92,7 +92,7 @@ export default { <div class="gl-flex-fill-1 gl-text-truncate">{{ item.description }}</div> </tooltip-on-truncate> </template> - <template #cell(owner)="{item}"> + <template #cell(owner)="{ item }"> <span class="trigger-owner sr-only">{{ item.owner.name }}</span> <user-avatar-link v-if="item.owner" @@ -102,11 +102,11 @@ export default { :img-alt="item.owner.name" /> </template> - <template #cell(lastUsed)="{item}"> + <template #cell(lastUsed)="{ item }"> <time-ago-tooltip v-if="item.lastUsed" :time="item.lastUsed" /> <span v-else>{{ __('Never') }}</span> </template> - <template #cell(actions)="{item}"> + <template #cell(actions)="{ item }"> <gl-button :title="s__('Pipelines|Edit')" icon="pencil" diff --git a/app/assets/javascripts/ci_settings_pipeline_triggers/index.js b/app/assets/javascripts/ci_settings_pipeline_triggers/index.js index 182d5ca5ffb..dc79bbb4d97 100644 --- a/app/assets/javascripts/ci_settings_pipeline_triggers/index.js +++ b/app/assets/javascripts/ci_settings_pipeline_triggers/index.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import TriggersList from './components/triggers_list.vue'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -const parseJsonArray = triggers => { +const parseJsonArray = (triggers) => { try { return convertObjectPropsToCamelCase(JSON.parse(triggers), { deep: true }); } catch { diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js index cb1935c863d..aa4d311527e 100644 --- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js @@ -81,17 +81,17 @@ export default class VariableList { this.initRow(rowEl); }); - this.$container.on('click', '.js-row-remove-button', e => { + this.$container.on('click', '.js-row-remove-button', (e) => { e.preventDefault(); this.removeRow($(e.currentTarget).closest('.js-row')); }); const inputSelector = Object.keys(this.inputMap) - .map(name => this.inputMap[name].selector) + .map((name) => this.inputMap[name].selector) .join(','); // Remove any empty rows except the last row - this.$container.on('blur', inputSelector, e => { + this.$container.on('blur', inputSelector, (e) => { const $row = $(e.currentTarget).closest('.js-row'); if ($row.is(':not(:last-child)') && !this.checkIfRowTouched($row)) { @@ -99,7 +99,7 @@ export default class VariableList { } }); - this.$container.on('input trigger-change', inputSelector, e => { + this.$container.on('input trigger-change', inputSelector, (e) => { // Always make sure there is an empty last row const $lastRow = this.$container.find('.js-row').last(); @@ -149,7 +149,7 @@ export default class VariableList { $rowClone.removeAttr('data-is-persisted'); // Reset the inputs to their defaults - Object.keys(this.inputMap).forEach(name => { + Object.keys(this.inputMap).forEach((name) => { const entry = this.inputMap[name]; $rowClone.find(entry.selector).val(entry.default); }); @@ -184,7 +184,7 @@ export default class VariableList { } checkIfRowTouched($row) { - return Object.keys(this.inputMap).some(name => { + return Object.keys(this.inputMap).some((name) => { // Row should not qualify as touched if only switches have been touched if (['protected', 'masked'].includes(name)) return false; @@ -223,14 +223,11 @@ export default class VariableList { getAllData() { // Ignore the last empty row because we don't want to try persist // a blank variable and run into validation problems. - const validRows = this.$container - .find('.js-row') - .toArray() - .slice(0, -1); + const validRows = this.$container.find('.js-row').toArray().slice(0, -1); - return validRows.map(rowEl => { + return validRows.map((rowEl) => { const resultant = {}; - Object.keys(this.inputMap).forEach(name => { + Object.keys(this.inputMap).forEach((name) => { const entry = this.inputMap[name]; const $input = $(rowEl).find(entry.selector); if ($input.length) { diff --git a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue index 83e9717041f..104d6672015 100644 --- a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue +++ b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue @@ -33,7 +33,7 @@ export default { }, filteredResults() { const lowerCasedSearchTerm = this.searchTerm.toLowerCase(); - return this.joinedEnvironments.filter(resultString => + return this.joinedEnvironments.filter((resultString) => resultString.toLowerCase().includes(lowerCasedSearchTerm), ); }, diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue index 07278bb442c..47b2745af08 100644 --- a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue +++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue @@ -69,7 +69,7 @@ export default { }, fields() { if (this.isGroup) { - return this.$options.fields.filter(field => field.key !== 'environment_scope'); + return this.$options.fields.filter((field) => field.key !== 'environment_scope'); } return this.$options.fields; }, diff --git a/app/assets/javascripts/ci_variable_list/store/actions.js b/app/assets/javascripts/ci_variable_list/store/actions.js index e3e9dac0a79..ac595fa0045 100644 --- a/app/assets/javascripts/ci_variable_list/store/actions.js +++ b/app/assets/javascripts/ci_variable_list/store/actions.js @@ -47,7 +47,7 @@ export const addVariable = ({ state, dispatch }) => { dispatch('receiveAddVariableSuccess'); dispatch('fetchVariables'); }) - .catch(error => { + .catch((error) => { createFlash(error.response.data[0]); dispatch('receiveAddVariableError', error); }); @@ -77,7 +77,7 @@ export const updateVariable = ({ state, dispatch }) => { dispatch('receiveUpdateVariableSuccess'); dispatch('fetchVariables'); }) - .catch(error => { + .catch((error) => { createFlash(error.response.data[0]); dispatch('receiveUpdateVariableError', error); }); @@ -132,7 +132,7 @@ export const deleteVariable = ({ dispatch, state }) => { dispatch('receiveDeleteVariableSuccess'); dispatch('fetchVariables'); }) - .catch(error => { + .catch((error) => { createFlash(error.response.data[0]); dispatch('receiveDeleteVariableError', error); }); @@ -150,7 +150,7 @@ export const fetchEnvironments = ({ dispatch, state }) => { dispatch('requestEnvironments'); return Api.environments(state.projectId) - .then(res => { + .then((res) => { dispatch('receiveEnvironmentsSuccess', prepareEnvironments(res.data)); }) .catch(() => { diff --git a/app/assets/javascripts/ci_variable_list/store/getters.js b/app/assets/javascripts/ci_variable_list/store/getters.js index 619ad54cad6..6570f455541 100644 --- a/app/assets/javascripts/ci_variable_list/store/getters.js +++ b/app/assets/javascripts/ci_variable_list/store/getters.js @@ -1,6 +1,6 @@ import { uniq } from 'lodash'; -export const joinedEnvironments = state => { - const scopesFromVariables = (state.variables || []).map(variable => variable.environment_scope); +export const joinedEnvironments = (state) => { + const scopesFromVariables = (state.variables || []).map((variable) => variable.environment_scope); return uniq(state.environments.concat(scopesFromVariables)).sort(); }; diff --git a/app/assets/javascripts/ci_variable_list/store/utils.js b/app/assets/javascripts/ci_variable_list/store/utils.js index f04530359e7..d9ca460a8e1 100644 --- a/app/assets/javascripts/ci_variable_list/store/utils.js +++ b/app/assets/javascripts/ci_variable_list/store/utils.js @@ -1,12 +1,12 @@ import { cloneDeep } from 'lodash'; import { displayText, types } from '../constants'; -const variableTypeHandler = type => +const variableTypeHandler = (type) => type === displayText.variableText ? types.variableType : types.fileType; -export const prepareDataForDisplay = variables => { +export const prepareDataForDisplay = (variables) => { const variablesToDisplay = []; - variables.forEach(variable => { + variables.forEach((variable) => { const variableCopy = variable; if (variableCopy.variable_type === types.variableType) { variableCopy.variable_type = displayText.variableText; @@ -42,4 +42,4 @@ export const prepareDataForApi = (variable, destroy = false) => { return variableCopy; }; -export const prepareEnvironments = environments => environments.map(e => e.name); +export const prepareEnvironments = (environments) => environments.map((e) => e.name); diff --git a/app/assets/javascripts/clone_panel.js b/app/assets/javascripts/clone_panel.js index 362e6c5c5ce..00bf54e1478 100644 --- a/app/assets/javascripts/clone_panel.js +++ b/app/assets/javascripts/clone_panel.js @@ -14,7 +14,7 @@ export default function initClonePanel() { $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active'); } - $('a', $cloneOptions).on('click', e => { + $('a', $cloneOptions).on('click', (e) => { e.preventDefault(); const $this = $(e.currentTarget); const url = $this.attr('href'); diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index a533a1a78e8..eb2128b2856 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -128,7 +128,7 @@ export default class Clusters { this.initPolling( 'fetchClusterEnvironments', - data => this.handleClusterEnvironmentsSuccess(data), + (data) => this.handleClusterEnvironmentsSuccess(data), () => this.handleEnvironmentsPollError(), ); } @@ -139,7 +139,7 @@ export default class Clusters { if (statusPath && !this.environments) { this.initPolling( 'fetchClusterStatus', - data => this.handleClusterStatusSuccess(data), + (data) => this.handleClusterStatusSuccess(data), () => this.handlePollError(), ); } @@ -248,15 +248,15 @@ export default class Clusters { addListeners() { eventHub.$on('installApplication', this.installApplication); - eventHub.$on('updateApplication', data => this.updateApplication(data)); - eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data)); - eventHub.$on('setKnativeDomain', data => this.setKnativeDomain(data)); - eventHub.$on('uninstallApplication', data => this.uninstallApplication(data)); - eventHub.$on('setCrossplaneProviderStack', data => this.setCrossplaneProviderStack(data)); - eventHub.$on('setIngressModSecurityEnabled', data => this.setIngressModSecurityEnabled(data)); - eventHub.$on('setIngressModSecurityMode', data => this.setIngressModSecurityMode(data)); - eventHub.$on('resetIngressModSecurityChanges', id => this.resetIngressModSecurityChanges(id)); - eventHub.$on('setFluentdSettings', data => this.setFluentdSettings(data)); + eventHub.$on('updateApplication', (data) => this.updateApplication(data)); + eventHub.$on('saveKnativeDomain', (data) => this.saveKnativeDomain(data)); + eventHub.$on('setKnativeDomain', (data) => this.setKnativeDomain(data)); + eventHub.$on('uninstallApplication', (data) => this.uninstallApplication(data)); + eventHub.$on('setCrossplaneProviderStack', (data) => this.setCrossplaneProviderStack(data)); + eventHub.$on('setIngressModSecurityEnabled', (data) => this.setIngressModSecurityEnabled(data)); + eventHub.$on('setIngressModSecurityMode', (data) => this.setIngressModSecurityMode(data)); + eventHub.$on('resetIngressModSecurityChanges', (id) => this.resetIngressModSecurityChanges(id)); + eventHub.$on('setFluentdSettings', (data) => this.setFluentdSettings(data)); // Add event listener to all the banner close buttons this.addBannerCloseHandler(this.unreachableContainer, 'unreachable'); this.addBannerCloseHandler(this.authenticationFailureContainer, 'authentication_failure'); @@ -343,12 +343,12 @@ export default class Clusters { checkForNewInstalls(prevApplicationMap, newApplicationMap) { const appTitles = Object.keys(newApplicationMap) .filter( - appId => + (appId) => newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED && prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED && prevApplicationMap[appId].status !== null, ) - .map(appId => newApplicationMap[appId].title); + .map((appId) => newApplicationMap[appId].title); if (appTitles.length > 0) { const text = sprintf( @@ -450,7 +450,7 @@ export default class Clusters { ); }); }) - .catch(error => this.store.updateAppProperty(appId, 'validationError', error)); + .catch((error) => this.store.updateAppProperty(appId, 'validationError', error)); } static validateInstallation(appId, params) { diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index fdffaa24d03..e096a29ce7f 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -461,7 +461,7 @@ export default { ) " > - <template #code="{content}"> + <template #code="{ content }"> <code>{{ content }}</code> </template> <template #link="{ content }"> diff --git a/app/assets/javascripts/clusters/components/fluentd_output_settings.vue b/app/assets/javascripts/clusters/components/fluentd_output_settings.vue index b37fc3894f8..84a39874000 100644 --- a/app/assets/javascripts/clusters/components/fluentd_output_settings.vue +++ b/app/assets/javascripts/clusters/components/fluentd_output_settings.vue @@ -127,7 +127,7 @@ export default { }); }, updateCurrentServerSideSettings(settings) { - Object.keys(settings).forEach(key => { + Object.keys(settings).forEach((key) => { if (this.currentServerSideSettings[key] === null) { this.currentServerSideSettings[key] = this[key]; } diff --git a/app/assets/javascripts/clusters/forms/stores/index.js b/app/assets/javascripts/clusters/forms/stores/index.js index ae082c07f26..87f1c05fdf9 100644 --- a/app/assets/javascripts/clusters/forms/stores/index.js +++ b/app/assets/javascripts/clusters/forms/stores/index.js @@ -4,7 +4,7 @@ import state from './state'; Vue.use(Vuex); -export const createStore = initialState => +export const createStore = (initialState) => new Vuex.Store({ state: state(initialState), }); diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index 88505eac3a9..5de487308c5 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -17,7 +17,7 @@ import { } from '../constants'; import transitionApplicationState from '../services/application_state_machine'; -const isApplicationInstalled = appStatus => APPLICATION_INSTALLED_STATUSES.includes(appStatus); +const isApplicationInstalled = (appStatus) => APPLICATION_INSTALLED_STATUSES.includes(appStatus); const applicationInitialState = { status: null, @@ -195,7 +195,7 @@ export default class ClusterStore { this.state.status = serverState.status; this.state.statusReason = serverState.status_reason; - serverState.applications.forEach(serverAppEntry => { + serverState.applications.forEach((serverAppEntry) => { const { name: appId, status, @@ -284,7 +284,7 @@ export default class ClusterStore { } updateEnvironments(environments = []) { - this.state.environments = environments.map(environment => ({ + this.state.environments = environments.map((environment) => ({ name: environment.name, project: environment.project, environmentPath: environment.environment_path, diff --git a/app/assets/javascripts/clusters/stores/new_cluster/index.js b/app/assets/javascripts/clusters/stores/new_cluster/index.js index ae082c07f26..87f1c05fdf9 100644 --- a/app/assets/javascripts/clusters/stores/new_cluster/index.js +++ b/app/assets/javascripts/clusters/stores/new_cluster/index.js @@ -4,7 +4,7 @@ import state from './state'; Vue.use(Vuex); -export const createStore = initialState => +export const createStore = (initialState) => new Vuex.Store({ state: state(initialState), }); diff --git a/app/assets/javascripts/clusters_list/components/ancestor_notice.vue b/app/assets/javascripts/clusters_list/components/ancestor_notice.vue index 7954fc61785..b241aa34283 100644 --- a/app/assets/javascripts/clusters_list/components/ancestor_notice.vue +++ b/app/assets/javascripts/clusters_list/components/ancestor_notice.vue @@ -1,11 +1,12 @@ <script> -import { GlLink, GlSprintf } from '@gitlab/ui'; +import { GlLink, GlSprintf, GlAlert } from '@gitlab/ui'; import { mapState } from 'vuex'; export default { components: { GlLink, GlSprintf, + GlAlert, }, computed: { ...mapState(['ancestorHelperPath', 'hasAncestorClusters']), @@ -14,21 +15,19 @@ export default { </script> <template> - <div v-if="hasAncestorClusters" class="bs-callout bs-callout-info"> - <p> - <gl-sprintf - :message=" - s__( - 'ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters. %{linkStart}More information%{linkEnd}', - ) - " - > - <template #link="{ content }"> - <gl-link :href="ancestorHelperPath"> - <strong>{{ content }}</strong> - </gl-link> - </template> - </gl-sprintf> - </p> - </div> + <gl-alert v-if="hasAncestorClusters" variant="info" :dismissible="false" class="gl-my-4"> + <gl-sprintf + :message=" + s__( + 'ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters. %{linkStart}More information%{linkEnd}', + ) + " + > + <template #link="{ content }"> + <gl-link :href="ancestorHelperPath"> + <strong>{{ content }}</strong> + </gl-link> + </template> + </gl-sprintf> + </gl-alert> </template> diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue index 08fd7db40a1..53eec5c8a0d 100644 --- a/app/assets/javascripts/clusters_list/components/clusters.vue +++ b/app/assets/javascripts/clusters_list/components/clusters.vue @@ -79,7 +79,7 @@ export default { { key: 'cluster_type', label: __('Cluster level'), - formatter: value => CLUSTER_TYPES[value], + formatter: (value) => CLUSTER_TYPES[value], }, ]; }, @@ -254,9 +254,7 @@ export default { <template #freeSpacePercentage>{{ totalCpuAndUsage(item.nodes).freeSpacePercentage }}</template> - <template #percentSymbol - >%</template - > + <template #percentSymbol>%</template> </gl-sprintf> </span> @@ -277,9 +275,7 @@ export default { <template #freeSpacePercentage>{{ totalMemoryAndUsage(item.nodes).freeSpacePercentage }}</template> - <template #percentSymbol - >%</template - > + <template #percentSymbol>%</template> </gl-sprintf> </span> @@ -293,7 +289,7 @@ export default { /> </template> - <template #cell(cluster_type)="{value}"> + <template #cell(cluster_type)="{ value }"> <gl-badge variant="muted"> {{ value }} </gl-badge> diff --git a/app/assets/javascripts/clusters_list/load_clusters.js b/app/assets/javascripts/clusters_list/load_clusters.js index 98bc5880898..01430230879 100644 --- a/app/assets/javascripts/clusters_list/load_clusters.js +++ b/app/assets/javascripts/clusters_list/load_clusters.js @@ -1,7 +1,7 @@ import Clusters from './components/clusters.vue'; import { createStore } from './store'; -export default Vue => { +export default (Vue) => { const el = document.querySelector('#js-clusters-list-app'); if (!el) { diff --git a/app/assets/javascripts/clusters_list/store/actions.js b/app/assets/javascripts/clusters_list/store/actions.js index 1be82988db0..97ed0a7ab37 100644 --- a/app/assets/javascripts/clusters_list/store/actions.js +++ b/app/assets/javascripts/clusters_list/store/actions.js @@ -13,11 +13,11 @@ const allNodesPresent = (clusters, retryCount) => { They may fail for reasons GitLab cannot control. MAX_REQUESTS will ensure this poll stops at some point. */ - return retryCount > MAX_REQUESTS || clusters.every(cluster => cluster.nodes != null); + return retryCount > MAX_REQUESTS || clusters.every((cluster) => cluster.nodes != null); }; export const reportSentryError = (_store, { error, tag }) => { - Sentry.withScope(scope => { + Sentry.withScope((scope) => { scope.setTag('javascript_clusters_list', tag); Sentry.captureException(error); }); @@ -30,7 +30,7 @@ export const fetchClusters = ({ state, commit, dispatch }) => { const poll = new Poll({ resource: { - fetchClusters: paginatedEndPoint => axios.get(paginatedEndPoint), + fetchClusters: (paginatedEndPoint) => axios.get(paginatedEndPoint), }, data: `${state.endpoint}?page=${state.page}`, method: 'fetchClusters', @@ -59,7 +59,7 @@ export const fetchClusters = ({ state, commit, dispatch }) => { dispatch('reportSentryError', { error, tag: 'fetchClustersSuccessCallback' }); } }, - errorCallback: response => { + errorCallback: (response) => { poll.stop(); commit(types.SET_LOADING_CLUSTERS, false); diff --git a/app/assets/javascripts/clusters_list/store/index.js b/app/assets/javascripts/clusters_list/store/index.js index c472d2f354c..47e17b3624b 100644 --- a/app/assets/javascripts/clusters_list/store/index.js +++ b/app/assets/javascripts/clusters_list/store/index.js @@ -6,7 +6,7 @@ import * as actions from './actions'; Vue.use(Vuex); -export const createStore = initialState => +export const createStore = (initialState) => new Vuex.Store({ actions, mutations, diff --git a/app/assets/javascripts/code_navigation/index.js b/app/assets/javascripts/code_navigation/index.js index fa5835245bc..38b3467dc33 100644 --- a/app/assets/javascripts/code_navigation/index.js +++ b/app/assets/javascripts/code_navigation/index.js @@ -3,7 +3,7 @@ import Vuex from 'vuex'; import createStore from './store'; import App from './components/app.vue'; -export default initialData => { +export default (initialData) => { const el = document.getElementById('js-code-navigation'); if (!el) return null; diff --git a/app/assets/javascripts/code_navigation/store/actions.js b/app/assets/javascripts/code_navigation/store/actions.js index 9a472ca014f..fb77a70de0b 100644 --- a/app/assets/javascripts/code_navigation/store/actions.js +++ b/app/assets/javascripts/code_navigation/store/actions.js @@ -34,7 +34,7 @@ export default { }, showBlobInteractionZones({ state }, path) { if (state.data && state.data[path]) { - Object.values(state.data[path]).forEach(d => addInteractionClass(path, d)); + Object.values(state.data[path]).forEach((d) => addInteractionClass(path, d)); } }, showDefinition({ commit, state }, { target: el }) { diff --git a/app/assets/javascripts/code_navigation/utils/index.js b/app/assets/javascripts/code_navigation/utils/index.js index bb33bc556af..6c078891ed4 100644 --- a/app/assets/javascripts/code_navigation/utils/index.js +++ b/app/assets/javascripts/code_navigation/utils/index.js @@ -1,7 +1,7 @@ export const cachedData = new Map(); export const getCurrentHoverElement = () => cachedData.get('current'); -export const setCurrentHoverElement = el => cachedData.set('current', el); +export const setCurrentHoverElement = (el) => cachedData.set('current', el); export const addInteractionClass = (path, d) => { const lineNumber = d.start_line + 1; @@ -10,7 +10,7 @@ export const addInteractionClass = (path, d) => { .querySelectorAll(`.blob-content #LC${lineNumber}, .line_content:not(.old) #LC${lineNumber}`); if (!lines?.length) return; - lines.forEach(line => { + lines.forEach((line) => { let charCount = 0; const el = [...line.childNodes].find(({ textContent }) => { if (charCount === d.start_char) return true; diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index b70f8d6e736..5f24a3c370a 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -28,7 +28,7 @@ export default class ImageFile { initViewModes() { const viewMode = viewModes[0]; $('.view-modes', this.file).removeClass('gl-display-none'); - $('.view-modes-menu', this.file).on('click', 'li', event => { + $('.view-modes-menu', this.file).on('click', 'li', (event) => { if (!$(event.currentTarget).hasClass('active')) { return this.activateViewMode(event.currentTarget.className); } @@ -56,15 +56,15 @@ export default class ImageFile { let dragging = false; const $body = $('body'); const $offsetEl = $el.parent(); - const dragStart = function() { + const dragStart = function () { dragging = true; $body.css('user-select', 'none'); }; - const dragStop = function() { + const dragStop = function () { dragging = false; $body.css('user-select', ''); }; - const dragMove = function(e) { + const dragMove = function (e) { const moveX = e.pageX || e.touches[0].pageX; const left = moveX - ($offsetEl.offset().left + padding); if (!dragging) return; @@ -73,11 +73,7 @@ export default class ImageFile { }; // eslint-disable-next-line @gitlab/no-global-event-off - $el - .off('mousedown') - .off('touchstart') - .on('mousedown', dragStart) - .on('touchstart', dragStart); + $el.off('mousedown').off('touchstart').on('mousedown', dragStart).on('touchstart', dragStart); // eslint-disable-next-line @gitlab/no-global-event-off $body @@ -109,9 +105,9 @@ export default class ImageFile { } views = { - 'two-up': function() { + 'two-up': function () { return $('.two-up.view .wrap', this.file).each((index, wrap) => { - $('img', wrap).each(function() { + $('img', wrap).each(function () { const currentWidth = $(this).width(); if (currentWidth > availWidth / 2) { return $(this).width(availWidth / 2); @@ -157,7 +153,7 @@ export default class ImageFile { }); }); }, - 'onion-skin': function() { + 'onion-skin': function () { let maxHeight, maxWidth; maxWidth = 0; maxHeight = 0; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index c8168afbcb0..24033634aad 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -21,7 +21,7 @@ export default () => { if (pipelineTableViewEl) { // Update MR and Commits tabs - pipelineTableViewEl.addEventListener('update-pipelines-count', event => { + pipelineTableViewEl.addEventListener('update-pipelines-count', (event) => { if ( event.detail.pipelines && event.detail.pipelines.count && diff --git a/app/assets/javascripts/commit_merge_requests.js b/app/assets/javascripts/commit_merge_requests.js index 3cdb1587a3b..e382356841c 100644 --- a/app/assets/javascripts/commit_merge_requests.js +++ b/app/assets/javascripts/commit_merge_requests.js @@ -50,7 +50,7 @@ export function createContent(mergeRequests) { if (mergeRequests.length === 0) { $content.text(s__('Commits|No related merge requests found')); } else { - mergeRequests.forEach(mergeRequest => { + mergeRequests.forEach((mergeRequest) => { const $header = createHeader($content.children().length, mergeRequests.length); const $item = createItem(mergeRequest); $content.append($header); @@ -66,7 +66,7 @@ export function fetchCommitMergeRequests() { axios .get($container.data('projectCommitPath')) - .then(response => { + .then((response) => { const $content = createContent(response.data); $container.html($content); diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index b18c109937d..f82bea134a3 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -85,10 +85,7 @@ export default class CommitsList { // Update commits count in the previous commits header. commitsCount += Number( - $(processedData) - .nextUntil('li.js-commit-header') - .first() - .find('li.commit').length, + $(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length, ); $commitsHeadersLast diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js index 2a1244149ff..f750c62103e 100644 --- a/app/assets/javascripts/commons/bootstrap.js +++ b/app/assets/javascripts/commons/bootstrap.js @@ -13,14 +13,10 @@ import 'bootstrap/js/dist/tab'; // custom jQuery functions $.fn.extend({ disable() { - return $(this) - .prop('disabled', true) - .addClass('disabled'); + return $(this).prop('disabled', true).addClass('disabled'); }, enable() { - return $(this) - .prop('disabled', false) - .removeClass('disabled'); + return $(this).prop('disabled', false).removeClass('disabled'); }, }); diff --git a/app/assets/javascripts/commons/nav/user_merge_requests.js b/app/assets/javascripts/commons/nav/user_merge_requests.js index 8e694cca6a1..84ab728274f 100644 --- a/app/assets/javascripts/commons/nav/user_merge_requests.js +++ b/app/assets/javascripts/commons/nav/user_merge_requests.js @@ -1,4 +1,4 @@ -import Api from '~/api'; +import { getUserCounts } from '~/rest_api'; let channel; @@ -11,7 +11,17 @@ function broadcastCount(newCount) { } function updateUserMergeRequestCounts(newCount) { - const mergeRequestsCountEl = document.querySelector('.merge-requests-count'); + const mergeRequestsCountEl = document.querySelector('.js-assigned-mr-count'); + mergeRequestsCountEl.textContent = newCount.toLocaleString(); +} + +function updateReviewerMergeRequestCounts(newCount) { + const mergeRequestsCountEl = document.querySelector('.js-reviewer-mr-count'); + mergeRequestsCountEl.textContent = newCount.toLocaleString(); +} + +function updateMergeRequestCounts(newCount) { + const mergeRequestsCountEl = document.querySelector('.js-merge-requests-count'); mergeRequestsCountEl.textContent = newCount.toLocaleString(); mergeRequestsCountEl.classList.toggle('hidden', Number(newCount) === 0); } @@ -20,14 +30,18 @@ function updateUserMergeRequestCounts(newCount) { * Refresh user counts (and broadcast if open) */ export function refreshUserMergeRequestCounts() { - return Api.userCounts() + return getUserCounts() .then(({ data }) => { - const count = data.merge_requests; + const assignedMergeRequests = data.assigned_merge_requests; + const reviewerMergeRequests = data.review_requested_merge_requests; + const fullCount = assignedMergeRequests + reviewerMergeRequests; - updateUserMergeRequestCounts(count); - broadcastCount(count); + updateUserMergeRequestCounts(assignedMergeRequests); + updateReviewerMergeRequestCounts(reviewerMergeRequests); + updateMergeRequestCounts(fullCount); + broadcastCount(fullCount); }) - .catch(ex => { + .catch((ex) => { console.error(ex); // eslint-disable-line no-console }); } @@ -59,8 +73,8 @@ export function openUserCountsBroadcast() { const currentUserId = typeof gon !== 'undefined' && gon && gon.current_user_id; if (currentUserId) { channel = new BroadcastChannel(`mr_count_channel_${currentUserId}`); - channel.onmessage = ev => { - updateUserMergeRequestCounts(ev.data); + channel.onmessage = (ev) => { + updateMergeRequestCounts(ev.data); }; } } diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js index 1c1f0664885..82384434e8f 100644 --- a/app/assets/javascripts/compare_autocomplete.js +++ b/app/assets/javascripts/compare_autocomplete.js @@ -9,7 +9,7 @@ import { fixTitle } from '~/tooltips'; import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) { - $('.js-compare-dropdown').each(function() { + $('.js-compare-dropdown').each(function () { const $dropdown = $(this); const selected = $dropdown.data('selected'); const $dropdownContainer = $dropdown.closest('.dropdown'); @@ -51,9 +51,7 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = ( .text(ref) .attr('data-ref', ref); if (ref.header != null) { - return $('<li />') - .addClass('dropdown-header') - .text(ref.header); + return $('<li />').addClass('dropdown-header').text(ref.header); } return $('<li />').append(link); }, @@ -65,7 +63,7 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = ( }, clicked: () => clickHandler($dropdown), }); - $filterInput.on('keyup', e => { + $filterInput.on('keyup', (e) => { const keyCode = e.keyCode || e.which; if (keyCode !== 13) return; const text = $filterInput.val(); @@ -74,7 +72,7 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = ( $dropdownContainer.removeClass('open'); }); - $dropdownContainer.on('click', '.dropdown-content a', e => { + $dropdownContainer.on('click', '.dropdown-content a', (e) => { $dropdown.prop('title', e.target.text.replace(/_+?/g, '-')); if ($dropdown.hasClass('has-tooltip')) { fixTitle($dropdown); diff --git a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue index 3a6707bc573..58fe022b794 100644 --- a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue +++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue @@ -61,7 +61,7 @@ export default { } }, normalizeProjectData(data) { - return data.map(p => ({ + return data.map((p) => ({ id: p.id, name: p.name_with_namespace, pathWithNamespace: p.path_with_namespace, @@ -78,7 +78,7 @@ export default { this.projects = this.normalizeProjectData(data); this.selectProject(this.projects[0]); }) - .catch(e => { + .catch((e) => { createFlash(__('Error fetching forked projects. Please try again.')); throw e; }); diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js index 4f7bc829b0c..5b9e70e3c09 100644 --- a/app/assets/javascripts/confirm_danger_modal.js +++ b/app/assets/javascripts/confirm_danger_modal.js @@ -47,7 +47,7 @@ function getModal($btn) { } export default function initConfirmDangerModal() { - $(document).on('click', '.js-confirm-danger', e => { + $(document).on('click', '.js-confirm-danger', (e) => { const $btn = $(e.target); const checkFieldName = $btn.data('checkFieldName'); const checkFieldCompareValue = $btn.data('checkCompareValue'); diff --git a/app/assets/javascripts/confirm_modal.js b/app/assets/javascripts/confirm_modal.js index bf2ea3ce38a..7a42b192b8a 100644 --- a/app/assets/javascripts/confirm_modal.js +++ b/app/assets/javascripts/confirm_modal.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import ConfirmModal from '~/vue_shared/components/confirm_modal.vue'; -const mountConfirmModal = optionalProps => +const mountConfirmModal = (optionalProps) => new Vue({ render(h) { return h(ConfirmModal, { diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js index 41988f321e5..f7d24c70864 100644 --- a/app/assets/javascripts/contextual_sidebar.js +++ b/app/assets/javascripts/contextual_sidebar.js @@ -43,7 +43,10 @@ export default class ContextualSidebar { $(document).trigger('content.resize'); }); - $(window).on('resize', debounce(() => this.render(), 100)); + $(window).on( + 'resize', + debounce(() => this.render(), 100), + ); } // See documentation: https://design.gitlab.com/regions/navigation#contextual-navigation diff --git a/app/assets/javascripts/contributors/components/contributors.vue b/app/assets/javascripts/contributors/components/contributors.vue index 3de1b2f0707..86580aa170b 100644 --- a/app/assets/javascripts/contributors/components/contributors.vue +++ b/app/assets/javascripts/contributors/components/contributors.vue @@ -39,7 +39,7 @@ export default { ...mapGetters(['showChart', 'parsedData']), masterChartData() { const data = {}; - this.xAxisRange.forEach(date => { + this.xAxisRange.forEach((date) => { data[date] = this.parsedData.total[date] || 0; }); return [ @@ -67,7 +67,7 @@ export default { const maxNumberOfIndividualContributorsCharts = 100; return Object.keys(this.parsedData.byAuthorEmail) - .map(email => { + .map((email) => { const author = this.parsedData.byAuthorEmail[email]; return { name: author.name, @@ -76,7 +76,7 @@ export default { dates: [ { name: __('Commits'), - data: this.xAxisRange.map(date => [date, author.dates[date] || 0]), + data: this.xAxisRange.map((date) => [date, author.dates[date] || 0]), }, ], }; @@ -101,7 +101,7 @@ export default { }, individualChartYAxisMax() { return this.individualChartsData.reduce((acc, item) => { - const values = item.dates[0].data.map(value => value[1]); + const values = item.dates[0].data.map((value) => value[1]); return Math.max(acc, ...values); }, 0); }, @@ -150,7 +150,7 @@ export default { }, setSvg(name) { return getSvgIconPathContent(name) - .then(path => { + .then((path) => { if (path) { this.$set(this.svgs, name, `path://${path}`); } @@ -177,7 +177,7 @@ export default { this.individualCharts.push(chart); }, setIndividualChartsZoom(options) { - this.charts.forEach(chart => + this.charts.forEach((chart) => chart.setOption( { dataZoom: { diff --git a/app/assets/javascripts/contributors/stores/actions.js b/app/assets/javascripts/contributors/stores/actions.js index f941c5aa944..72aae3af692 100644 --- a/app/assets/javascripts/contributors/stores/actions.js +++ b/app/assets/javascripts/contributors/stores/actions.js @@ -8,8 +8,8 @@ export const fetchChartData = ({ commit }, endpoint) => { return service .fetchChartData(endpoint) - .then(res => res.data) - .then(data => { + .then((res) => res.data) + .then((data) => { commit(types.SET_CHART_DATA, data); commit(types.SET_LOADING_STATE, false); }) diff --git a/app/assets/javascripts/contributors/stores/getters.js b/app/assets/javascripts/contributors/stores/getters.js index 9022179d6c7..45b569066f8 100644 --- a/app/assets/javascripts/contributors/stores/getters.js +++ b/app/assets/javascripts/contributors/stores/getters.js @@ -1,6 +1,6 @@ -export const showChart = state => Boolean(!state.loading && state.chartData); +export const showChart = (state) => Boolean(!state.loading && state.chartData); -export const parsedData = state => { +export const parsedData = (state) => { const byAuthorEmail = {}; const total = {}; diff --git a/app/assets/javascripts/contributors/utils.js b/app/assets/javascripts/contributors/utils.js index 7d8932ce495..fa3f8154dfe 100644 --- a/app/assets/javascripts/contributors/utils.js +++ b/app/assets/javascripts/contributors/utils.js @@ -9,7 +9,7 @@ import { getMonthNames } from '~/lib/utils/datetime_utility'; * xAxisLabelFormatter('02-12-2019') will return 'Feb' * xAxisLabelFormatter('07-12-2019') will return 'Jul' */ -export const xAxisLabelFormatter = val => { +export const xAxisLabelFormatter = (val) => { const date = new Date(val); const month = date.getUTCMonth(); const year = date.getUTCFullYear(); @@ -21,7 +21,7 @@ export const xAxisLabelFormatter = val => { * @param {Date} * @returns {String} - formatted value */ -export const dateFormatter = date => { +export const dateFormatter = (date) => { const year = date.getUTCFullYear(); const month = date.getUTCMonth(); const day = date.getUTCDate(); diff --git a/app/assets/javascripts/create_cluster/components/cluster_form_dropdown.vue b/app/assets/javascripts/create_cluster/components/cluster_form_dropdown.vue index 1e3a19b9da1..7f4c3635119 100644 --- a/app/assets/javascripts/create_cluster/components/cluster_form_dropdown.vue +++ b/app/assets/javascripts/create_cluster/components/cluster_form_dropdown.vue @@ -6,9 +6,9 @@ import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_searc import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue'; import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue'; -const toArray = value => (isNil(value) ? [] : [].concat(value)); -const itemsProp = (items, prop) => items.map(item => item[prop]); -const defaultSearchFn = (searchQuery, labelProp) => item => +const toArray = (value) => (isNil(value) ? [] : [].concat(value)); +const itemsProp = (items, prop) => items.map((item) => item[prop]); +const defaultSearchFn = (searchQuery, labelProp) => (item) => item[labelProp].toLowerCase().indexOf(searchQuery) > -1; export default { @@ -135,7 +135,7 @@ export default { const valueList = toArray(this.value); const items = this.getItemsOrEmptyList(); - return items.filter(item => valueList.some(value => item[valueProp] === value)); + return items.filter((item) => valueList.some((value) => item[valueProp] === value)); }, selectedItemsLabels() { return itemsProp(this.selectedItems, this.labelProperty).join(', '); diff --git a/app/assets/javascripts/create_cluster/eks_cluster/index.js b/app/assets/javascripts/create_cluster/eks_cluster/index.js index 6d1034b4a72..38b7eefd15b 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/index.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/index.js @@ -6,7 +6,7 @@ import createStore from './store'; Vue.use(Vuex); -export default el => { +export default (el) => { const { gitlabManagedClusterHelpPath, namespacePerEnvironmentHelpPath, diff --git a/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js b/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js index 58568b5dedb..c2b59191997 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js @@ -37,7 +37,7 @@ export const fetchVpcs = ({ region }) => { .describeVpcs() .promise() .then(({ Vpcs: vpcs }) => - vpcs.map(vpc => ({ + vpcs.map((vpc) => ({ value: vpc.VpcId, name: lookupVpcName(vpc), })), diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js index b182d4dff13..55576efd3b8 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js @@ -5,7 +5,7 @@ import axios from '~/lib/utils/axios_utils'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -const getErrorMessage = data => { +const getErrorMessage = (data) => { const errorKey = Object.keys(data)[0]; return data[errorKey][0]; @@ -42,7 +42,7 @@ export const createRole = ({ dispatch, state: { createRolePath } }, payload) => dispatch('createRoleSuccess', awsData); }) - .catch(error => { + .catch((error) => { let message = error; if (error?.response?.data?.message) { message = error.response.data.message; diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/state.js b/app/assets/javascripts/create_cluster/eks_cluster/store/state.js index c957eca1f7a..c906ddf9011 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/store/state.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/store/state.js @@ -1,6 +1,6 @@ import { KUBERNETES_VERSIONS } from '../constants'; -const kubernetesVersion = KUBERNETES_VERSIONS.find(version => version.default).value; +const kubernetesVersion = KUBERNETES_VERSIONS.find((version) => version.default).value; export default () => ({ createRolePath: null, diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_dropdown_mixin.js b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_dropdown_mixin.js index 3b91ce63744..f9d0d86e381 100644 --- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_dropdown_mixin.js +++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_dropdown_mixin.js @@ -42,13 +42,13 @@ export default { return []; } - return this.items.filter(item => item.name.toLowerCase().indexOf(this.searchQuery) > -1); + return this.items.filter((item) => item.name.toLowerCase().indexOf(this.searchQuery) > -1); }, }, methods: { fetchSuccessHandler() { if (this.defaultValue) { - const itemToSelect = this.items.find(item => item.name === this.defaultValue); + const itemToSelect = this.items.find((item) => item.name === this.defaultValue); if (itemToSelect) { this.setItem(itemToSelect.name); diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue index d6deda25752..6b18455bfcc 100644 --- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue +++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue @@ -57,9 +57,7 @@ export default { if (this.hasZone) { this.isLoading = true; - this.fetchMachineTypes() - .then(this.fetchSuccessHandler) - .catch(this.fetchFailureHandler); + this.fetchMachineTypes().then(this.fetchSuccessHandler).catch(this.fetchFailureHandler); } }, }, diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue index 522fef423af..acbc4d1b3bc 100644 --- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue +++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue @@ -99,16 +99,14 @@ export default { created() { this.isLoading = true; - this.fetchProjects() - .then(this.fetchSuccessHandler) - .catch(this.fetchFailureHandler); + this.fetchProjects().then(this.fetchSuccessHandler).catch(this.fetchFailureHandler); }, methods: { ...mapActions(['fetchProjects', 'setIsValidatingProjectBilling', 'validateProjectBilling']), ...mapActions({ setItem: 'setProject' }), fetchSuccessHandler() { if (this.defaultValue) { - const projectToSelect = this.items.find(item => item.projectId === this.defaultValue); + const projectToSelect = this.items.find((item) => item.projectId === this.defaultValue); if (projectToSelect) { this.setItem(projectToSelect); @@ -175,9 +173,7 @@ export default { <gl-sprintf :message="helpText"> <template #linkToBilling="{ content }"> <gl-link - :href=" - 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral' - " + :href="'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral'" target="_blank" >{{ content }} <gl-icon name="external-link" /></gl-link> diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_zone_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_zone_dropdown.vue index b60a5be2e63..daab42c7e60 100644 --- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_zone_dropdown.vue +++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_zone_dropdown.vue @@ -46,9 +46,7 @@ export default { if (!isValidating && this.projectHasBillingEnabled) { this.isLoading = true; - this.fetchZones() - .then(this.fetchSuccessHandler) - .catch(this.fetchFailureHandler); + this.fetchZones().then(this.fetchSuccessHandler).catch(this.fetchFailureHandler); } }, }, diff --git a/app/assets/javascripts/create_cluster/gke_cluster/gapi_loader.js b/app/assets/javascripts/create_cluster/gke_cluster/gapi_loader.js new file mode 100644 index 00000000000..b5f92fed8eb --- /dev/null +++ b/app/assets/javascripts/create_cluster/gke_cluster/gapi_loader.js @@ -0,0 +1,24 @@ +// This is a helper module to lazily import the google APIs for the GKE cluster +// integration without introducing an indirect global dependency on an +// initialized window.gapi object. +export default () => { + if (window.gapiPromise === undefined) { + // first time loading the module + window.gapiPromise = new Promise((resolve, reject) => { + // this callback is set as a query param to script.src URL + window.onGapiLoad = () => { + resolve(window.gapi); + }; + + const script = document.createElement('script'); + // do not use script.onload, because gapi continues to load after the initial script load + script.type = 'text/javascript'; + script.async = true; + script.src = 'https://apis.google.com/js/api.js?onload=onGapiLoad'; + script.onerror = reject; + document.head.appendChild(script); + }); + } + + return window.gapiPromise; +}; diff --git a/app/assets/javascripts/create_cluster/gke_cluster/index.js b/app/assets/javascripts/create_cluster/gke_cluster/index.js index b9316353072..c02173fc510 100644 --- a/app/assets/javascripts/create_cluster/gke_cluster/index.js +++ b/app/assets/javascripts/create_cluster/gke_cluster/index.js @@ -1,10 +1,10 @@ -/* global gapi */ import Vue from 'vue'; import { deprecatedCreateFlash as Flash } from '~/flash'; import GkeProjectIdDropdown from './components/gke_project_id_dropdown.vue'; import GkeZoneDropdown from './components/gke_zone_dropdown.vue'; import GkeMachineTypeDropdown from './components/gke_machine_type_dropdown.vue'; import GkeSubmitButton from './components/gke_submit_button.vue'; +import gapiLoader from './gapi_loader'; import store from './store'; @@ -22,7 +22,7 @@ const mountComponent = (entryPoint, component, componentName, extraProps = {}) = components: { [componentName]: component, }, - render: createElement => + render: (createElement) => createElement(componentName, { props: { fieldName: hiddenInput.getAttribute('name'), @@ -63,7 +63,7 @@ const gkeDropdownErrorHandler = () => { Flash(CONSTANTS.GCP_API_ERROR); }; -const initializeGapiClient = () => { +const initializeGapiClient = (gapi) => () => { const el = document.querySelector('.js-gke-cluster-creation'); if (!el) return false; @@ -86,13 +86,9 @@ const initializeGapiClient = () => { .catch(gkeDropdownErrorHandler); }; -const initGkeDropdowns = () => { - if (!gapi) { - gkeDropdownErrorHandler(); - return false; - } - - return gapi.load('client', initializeGapiClient); -}; +const initGkeDropdowns = () => + gapiLoader() + .then((gapi) => gapi.load('client', initializeGapiClient(gapi))) + .catch(gkeDropdownErrorHandler); export default initGkeDropdowns; diff --git a/app/assets/javascripts/create_cluster/gke_cluster/store/actions.js b/app/assets/javascripts/create_cluster/gke_cluster/store/actions.js index f0c41d1d230..8977053297a 100644 --- a/app/assets/javascripts/create_cluster/gke_cluster/store/actions.js +++ b/app/assets/javascripts/create_cluster/gke_cluster/store/actions.js @@ -1,19 +1,19 @@ -/* global gapi */ import * as types from './mutation_types'; +import gapiLoader from '../gapi_loader'; const gapiResourceListRequest = ({ resource, params, commit, mutation, payloadKey }) => new Promise((resolve, reject) => { const request = resource.list(params); return request.then( - resp => { + (resp) => { const { result } = resp; commit(mutation, result[payloadKey]); resolve(); }, - resp => { + (resp) => { reject(resp); }, ); @@ -36,57 +36,64 @@ export const setIsValidatingProjectBilling = ({ commit }, isValidatingProjectBil }; export const fetchProjects = ({ commit }) => - gapiResourceListRequest({ - resource: gapi.client.cloudresourcemanager.projects, - params: {}, - commit, - mutation: types.SET_PROJECTS, - payloadKey: 'projects', - }); + gapiLoader().then((gapi) => + gapiResourceListRequest({ + resource: gapi.client.cloudresourcemanager.projects, + params: {}, + commit, + mutation: types.SET_PROJECTS, + payloadKey: 'projects', + }), + ); export const validateProjectBilling = ({ dispatch, commit, state }) => - new Promise((resolve, reject) => { - const request = gapi.client.cloudbilling.projects.getBillingInfo({ - name: `projects/${state.selectedProject.projectId}`, - }); + gapiLoader() + .then((gapi) => { + const request = gapi.client.cloudbilling.projects.getBillingInfo({ + name: `projects/${state.selectedProject.projectId}`, + }); - commit(types.SET_ZONE, ''); - commit(types.SET_MACHINE_TYPE, ''); + commit(types.SET_ZONE, ''); + commit(types.SET_MACHINE_TYPE, ''); - return request.then( - resp => { + return request; + }) + .then( + (resp) => { const { billingEnabled } = resp.result; commit(types.SET_PROJECT_BILLING_STATUS, Boolean(billingEnabled)); dispatch('setIsValidatingProjectBilling', false); - resolve(); }, - resp => { + (errorResp) => { dispatch('setIsValidatingProjectBilling', false); - reject(resp); + return errorResp; }, ); - }); export const fetchZones = ({ commit, state }) => - gapiResourceListRequest({ - resource: gapi.client.compute.zones, - params: { - project: state.selectedProject.projectId, - }, - commit, - mutation: types.SET_ZONES, - payloadKey: 'items', - }); + gapiLoader().then((gapi) => + gapiResourceListRequest({ + resource: gapi.client.compute.zones, + params: { + project: state.selectedProject.projectId, + }, + commit, + mutation: types.SET_ZONES, + payloadKey: 'items', + }), + ); export const fetchMachineTypes = ({ commit, state }) => - gapiResourceListRequest({ - resource: gapi.client.compute.machineTypes, - params: { - project: state.selectedProject.projectId, - zone: state.selectedZone, - }, - commit, - mutation: types.SET_MACHINE_TYPES, - payloadKey: 'items', - }); + gapiLoader().then((gapi) => + gapiResourceListRequest({ + resource: gapi.client.compute.machineTypes, + params: { + project: state.selectedProject.projectId, + zone: state.selectedZone, + }, + commit, + mutation: types.SET_MACHINE_TYPES, + payloadKey: 'items', + }), + ); diff --git a/app/assets/javascripts/create_cluster/gke_cluster/store/getters.js b/app/assets/javascripts/create_cluster/gke_cluster/store/getters.js index 4d4cd223832..99f8393ffdb 100644 --- a/app/assets/javascripts/create_cluster/gke_cluster/store/getters.js +++ b/app/assets/javascripts/create_cluster/gke_cluster/store/getters.js @@ -1,5 +1,5 @@ -export const hasProject = state => Boolean(state.selectedProject.projectId); -export const hasZone = state => Boolean(state.selectedZone); -export const hasMachineType = state => Boolean(state.selectedMachineType); +export const hasProject = (state) => Boolean(state.selectedProject.projectId); +export const hasZone = (state) => Boolean(state.selectedZone); +export const hasMachineType = (state) => Boolean(state.selectedMachineType); export const hasValidData = (state, getters) => Boolean(state.projectHasBillingEnabled) && getters.hasZone && getters.hasMachineType; diff --git a/app/assets/javascripts/create_cluster/gke_cluster_namespace/index.js b/app/assets/javascripts/create_cluster/gke_cluster_namespace/index.js index 0ec4d8807b0..2b3dfb99328 100644 --- a/app/assets/javascripts/create_cluster/gke_cluster_namespace/index.js +++ b/app/assets/javascripts/create_cluster/gke_cluster_namespace/index.js @@ -12,7 +12,7 @@ const setDisabled = (el, isDisabled) => { } }; -const setState = glManagedCheckbox => { +const setState = (glManagedCheckbox) => { const glManaged = document.querySelector('.js-namespace-prefixed'); const selfManaged = document.querySelector('.js-namespace'); diff --git a/app/assets/javascripts/create_cluster/init_create_cluster.js b/app/assets/javascripts/create_cluster/init_create_cluster.js index 2b09771d772..f97da3d55db 100644 --- a/app/assets/javascripts/create_cluster/init_create_cluster.js +++ b/app/assets/javascripts/create_cluster/init_create_cluster.js @@ -4,11 +4,11 @@ import PersistentUserCallout from '~/persistent_user_callout'; const newClusterViews = [':clusters:new', ':clusters:create_gcp', ':clusters:create_user']; -const isProjectLevelCluster = page => page.startsWith('project:clusters'); +const isProjectLevelCluster = (page) => page.startsWith('project:clusters'); -export default document => { +export default (document) => { const { page } = document.body.dataset; - const isNewClusterView = newClusterViews.some(view => page.endsWith(view)); + const isNewClusterView = newClusterViews.some((view) => page.endsWith(view)); if (!isNewClusterView) { return; @@ -19,6 +19,10 @@ export default document => { initGkeDropdowns(); + if (isProjectLevelCluster(page)) { + initGkeNamespace(); + } + import(/* webpackChunkName: 'eks_cluster' */ '~/create_cluster/eks_cluster') .then(({ default: initCreateEKSCluster }) => { const el = document.querySelector('.js-create-eks-cluster-form-container'); @@ -28,8 +32,4 @@ export default document => { } }) .catch(() => {}); - - if (isProjectLevelCluster(page)) { - initGkeNamespace(); - } }; diff --git a/app/assets/javascripts/create_cluster/store/cluster_dropdown/actions.js b/app/assets/javascripts/create_cluster/store/cluster_dropdown/actions.js index 5d250b2e29e..669b0dcc732 100644 --- a/app/assets/javascripts/create_cluster/store/cluster_dropdown/actions.js +++ b/app/assets/javascripts/create_cluster/store/cluster_dropdown/actions.js @@ -1,6 +1,6 @@ import * as types from './mutation_types'; -export default fetchItems => ({ +export default (fetchItems) => ({ requestItems: ({ commit }) => commit(types.REQUEST_ITEMS), receiveItemsSuccess: ({ commit }, payload) => commit(types.RECEIVE_ITEMS_SUCCESS, payload), receiveItemsError: ({ commit }, payload) => commit(types.RECEIVE_ITEMS_ERROR, payload), @@ -8,7 +8,7 @@ export default fetchItems => ({ dispatch('requestItems'); return fetchItems(payload) - .then(items => dispatch('receiveItemsSuccess', { items })) - .catch(error => dispatch('receiveItemsError', { error })); + .then((items) => dispatch('receiveItemsSuccess', { items })) + .catch((error) => dispatch('receiveItemsError', { error })); }, }); diff --git a/app/assets/javascripts/create_item_dropdown.js b/app/assets/javascripts/create_item_dropdown.js index 75e8523adfa..1472adf458b 100644 --- a/app/assets/javascripts/create_item_dropdown.js +++ b/app/assets/javascripts/create_item_dropdown.js @@ -47,7 +47,7 @@ export default class CreateItemDropdown { return escape(item.id); }, onFilter: this.toggleCreateNewButton.bind(this), - clicked: options => { + clicked: (options) => { options.e.preventDefault(); this.onSelect(); }, @@ -79,7 +79,7 @@ export default class CreateItemDropdown { this.getDataOption(term, (data = []) => { // Ensure the selected item isn't already in the data to avoid duplicates const alreadyHasSelectedItem = - this.selectedItem && data.some(item => item.id === this.selectedItem.id); + this.selectedItem && data.some((item) => item.id === this.selectedItem.id); let uniqueData = data; if (!alreadyHasSelectedItem) { diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js index 0d53efe8689..07fe2c7e01f 100644 --- a/app/assets/javascripts/create_label.js +++ b/app/assets/javascripts/create_label.js @@ -46,7 +46,7 @@ export default class CreateLabelDropdown { addBinding() { const self = this; - this.$colorSuggestions.on('click', function(e) { + this.$colorSuggestions.on('click', function (e) { const $this = $(this); self.addColorValue(e, $this); }); @@ -56,7 +56,7 @@ export default class CreateLabelDropdown { this.$dropdownBack.on('click', this.resetForm.bind(this)); - this.$cancelButton.on('click', e => { + this.$cancelButton.on('click', (e) => { e.preventDefault(); e.stopPropagation(); @@ -72,10 +72,7 @@ export default class CreateLabelDropdown { e.stopPropagation(); this.$newColorField.val($this.data('color')).trigger('change'); - this.$colorPreview - .css('background-color', $this.data('color')) - .parent() - .addClass('is-active'); + this.$colorPreview.css('background-color', $this.data('color')).parent().addClass('is-active'); } enableLabelCreateButton() { @@ -94,10 +91,7 @@ export default class CreateLabelDropdown { this.$addList.prop('checked', this.addListDefault); - this.$colorPreview - .css('background-color', '') - .parent() - .removeClass('is-active'); + this.$colorPreview.css('background-color', '').parent().removeClass('is-active'); } saveLabel(e) { @@ -111,7 +105,7 @@ export default class CreateLabelDropdown { title: this.$newLabelField.val(), color: this.$newColorField.val(), }, - label => { + (label) => { this.$newLabelCreateButton.enable(); if (label.message) { @@ -121,7 +115,7 @@ export default class CreateLabelDropdown { errors = label.message; } else { errors = Object.keys(label.message) - .map(key => `${humanize(key)} ${label.message[key].join(', ')}`) + .map((key) => `${humanize(key)} ${label.message[key].join(', ')}`) .join('<br/>'); } diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index 49091f5f140..aaaa7055799 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -191,7 +191,7 @@ export default class CreateMergeRequestDropdown { static findByValue(objects, ref, returnFirstMatch = false) { if (!objects || !objects.length) return false; if (objects.indexOf(ref) > -1) return ref; - if (returnFirstMatch) return objects.find(item => new RegExp(`^${ref}`).test(item)); + if (returnFirstMatch) return objects.find((item) => new RegExp(`^${ref}`).test(item)); return false; } @@ -356,7 +356,7 @@ export default class CreateMergeRequestDropdown { event.preventDefault(); if (isConfidentialIssue() && !event.target.classList.contains('js-create-target')) { - this.droplab.hooks.forEach(hook => hook.list.toggle()); + this.droplab.hooks.forEach((hook) => hook.list.toggle()); return; } @@ -404,8 +404,8 @@ export default class CreateMergeRequestDropdown { const inputClasses = ['gl-field-error-outline', 'gl-field-success-outline']; const messageClasses = ['text-muted', 'text-danger', 'text-success']; - inputClasses.forEach(cssClass => input.classList.remove(cssClass)); - messageClasses.forEach(cssClass => message.classList.remove(cssClass)); + inputClasses.forEach((cssClass) => input.classList.remove(cssClass)); + messageClasses.forEach((cssClass) => message.classList.remove(cssClass)); message.style.display = 'none'; } diff --git a/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue b/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue index 6f8455e4bcf..f3fa28dc2f3 100644 --- a/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue +++ b/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue @@ -22,7 +22,7 @@ let cancelTokenSource; function backOffRequest(makeRequestCallback) { return backOff((next, stop) => { makeRequestCallback() - .then(resp => { + .then((resp) => { if (resp.status === statusCodes.OK) { stop(resp); } else { @@ -32,7 +32,7 @@ function backOffRequest(makeRequestCallback) { // If the request is cancelled by axios // then consider it as noop so that its not // caught by subsequent catches - .catch(thrown => (axios.isCancel(thrown) ? undefined : stop(thrown))); + .catch((thrown) => (axios.isCancel(thrown) ? undefined : stop(thrown))); }, VALIDATION_REQUEST_TIMEOUT); } @@ -144,7 +144,7 @@ export default { // as well. cancelTokenSource = axiosCancelToken.source(); this.requestValidation(this.query, cancelTokenSource.token) - .then(res => { + .then((res) => { const response = res.data; const { valid, error } = response.query; if (response.success) { diff --git a/app/assets/javascripts/custom_metrics/constants.js b/app/assets/javascripts/custom_metrics/constants.js index 2526445fdf9..cd2ac6d8390 100644 --- a/app/assets/javascripts/custom_metrics/constants.js +++ b/app/assets/javascripts/custom_metrics/constants.js @@ -4,9 +4,9 @@ export const queryTypes = { system: 'system', }; -export const formDataValidator = val => { +export const formDataValidator = (val) => { const fieldNames = Object.keys(val); const requiredFields = ['title', 'query', 'yLabel', 'unit', 'group', 'legend']; - return requiredFields.every(name => fieldNames.includes(name)); + return requiredFields.every((name) => fieldNames.includes(name)); }; diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue index b93a7d3c4f9..f52438ca2cb 100644 --- a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue @@ -30,8 +30,6 @@ export default { {{ time.seconds }} <span> {{ s__('Time|s') }} </span> </template> </template> - <template v-else> - -- - </template> + <template v-else> -- </template> </span> </template> diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 70ebe91a3b2..bd5a6cc40c4 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -78,7 +78,7 @@ export default () => { $dropdown .find('li a') .off('click') - .on('click', e => { + .on('click', (e) => { e.preventDefault(); const $target = $(e.currentTarget); this.startDate = $target.data('value'); @@ -94,7 +94,7 @@ export default () => { this.service .fetchCycleAnalyticsData(fetchOptions) - .then(response => { + .then((response) => { this.store.setCycleAnalyticsData(response); this.selectDefaultStage(); this.initDropdown(); @@ -128,7 +128,7 @@ export default () => { startDate: this.startDate, projectIds: this.selectedProjectIds, }) - .then(response => { + .then((response) => { this.isEmptyStage = !response.events.length; this.store.setStageEvents(response.events, stage); this.isLoadingStage = false; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js index babbfe93082..d7fcda24352 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js @@ -17,7 +17,7 @@ export default class CycleAnalyticsService { 'cycle_analytics[project_ids]': projectIds, }, }) - .then(x => x.data); + .then((x) => x.data); } fetchStageData(options) { @@ -30,6 +30,6 @@ export default class CycleAnalyticsService { 'cycle_analytics[project_ids]': projectIds, }, }) - .then(x => x.data); + .then((x) => x.data); } } diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js index 3a160d0532c..7b04494892e 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js @@ -42,11 +42,11 @@ export default { newData.stages = data.stats || []; newData.summary = data.summary || []; - newData.summary.forEach(item => { + newData.summary.forEach((item) => { item.value = item.value || '-'; }); - newData.stages.forEach(item => { + newData.stages.forEach((item) => { const stageSlug = dasherize(item.name.toLowerCase()); item.active = false; item.isUserAllowed = data.permissions[stageSlug]; @@ -64,7 +64,7 @@ export default { this.state.hasError = state; }, deactivateAllStages() { - this.state.stages.forEach(stage => { + this.state.stages.forEach((stage) => { stage.active = false; }); }, @@ -78,7 +78,7 @@ export default { decorateEvents(events, stage) { const newEvents = []; - events.forEach(item => { + events.forEach((item) => { if (!item) return; const eventItem = { ...DEFAULT_EVENT_OBJECTS[stage.slug], ...item }; @@ -107,6 +107,6 @@ export default { return newEvents; }, currentActiveStage() { - return this.state.stages.find(stage => stage.active); + return this.state.stages.find((stage) => stage.active); }, }; diff --git a/app/assets/javascripts/deploy_freeze/store/actions.js b/app/assets/javascripts/deploy_freeze/store/actions.js index 2fbbba5a128..9a75c3cad2f 100644 --- a/app/assets/javascripts/deploy_freeze/store/actions.js +++ b/app/assets/javascripts/deploy_freeze/store/actions.js @@ -28,7 +28,7 @@ export const addFreezePeriod = ({ state, dispatch, commit }) => { commit(types.RESET_MODAL); dispatch('fetchFreezePeriods'); }) - .catch(error => { + .catch((error) => { createFlash(__('Error: Unable to create deploy freeze')); dispatch('receiveAddFreezePeriodError', error); }); diff --git a/app/assets/javascripts/deploy_freeze/store/index.js b/app/assets/javascripts/deploy_freeze/store/index.js index ca7ea8c783c..2da7ed31a13 100644 --- a/app/assets/javascripts/deploy_freeze/store/index.js +++ b/app/assets/javascripts/deploy_freeze/store/index.js @@ -6,7 +6,7 @@ import createState from './state'; Vue.use(Vuex); -export default initialState => +export default (initialState) => new Vuex.Store({ actions, mutations, diff --git a/app/assets/javascripts/deploy_freeze/store/mutations.js b/app/assets/javascripts/deploy_freeze/store/mutations.js index 89ce1dc5428..3b34f3950e6 100644 --- a/app/assets/javascripts/deploy_freeze/store/mutations.js +++ b/app/assets/javascripts/deploy_freeze/store/mutations.js @@ -4,7 +4,7 @@ import * as types from './mutation_types'; const formatTimezoneName = (freezePeriod, timezoneList) => convertObjectPropsToCamelCase({ ...freezePeriod, - cron_timezone: timezoneList.find(tz => tz.identifier === freezePeriod.cron_timezone)?.name, + cron_timezone: timezoneList.find((tz) => tz.identifier === freezePeriod.cron_timezone)?.name, }); export default { @@ -14,7 +14,7 @@ export default { [types.RECEIVE_FREEZE_PERIODS_SUCCESS](state, freezePeriods) { state.isLoading = false; - state.freezePeriods = freezePeriods.map(freezePeriod => + state.freezePeriods = freezePeriods.map((freezePeriod) => formatTimezoneName(freezePeriod, state.timezoneData), ); }, diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue index 0ac16e6b6a0..92e80d15902 100644 --- a/app/assets/javascripts/deploy_keys/components/app.vue +++ b/app/assets/javascripts/deploy_keys/components/app.vue @@ -39,7 +39,7 @@ export default { }, computed: { tabs() { - return Object.keys(this.$options.scopes).map(scope => { + return Object.keys(this.$options.scopes).map((scope) => { const count = Array.isArray(this.keys[scope]) ? this.keys[scope].length : null; return { @@ -81,7 +81,7 @@ export default { return this.service .getKeys() - .then(data => { + .then((data) => { this.isLoading = false; this.store.keys = data; }) diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue index 16eee094108..3ddaba7abcc 100644 --- a/app/assets/javascripts/deploy_keys/components/key.vue +++ b/app/assets/javascripts/deploy_keys/components/key.vue @@ -48,7 +48,7 @@ export default { if (this.projectId !== null) { const indexOfCurrentProject = projects.findIndex( - project => + (project) => project && project.project && project.project.id && diff --git a/app/assets/javascripts/deploy_keys/service/index.js b/app/assets/javascripts/deploy_keys/service/index.js index 10333752936..2837fc8ed88 100644 --- a/app/assets/javascripts/deploy_keys/service/index.js +++ b/app/assets/javascripts/deploy_keys/service/index.js @@ -6,14 +6,14 @@ export default class DeployKeysService { } getKeys() { - return axios.get(this.endpoint).then(response => response.data); + return axios.get(this.endpoint).then((response) => response.data); } enableKey(id) { - return axios.put(`${this.endpoint}/${id}/enable`).then(response => response.data); + return axios.put(`${this.endpoint}/${id}/enable`).then((response) => response.data); } disableKey(id) { - return axios.put(`${this.endpoint}/${id}/disable`).then(response => response.data); + return axios.put(`${this.endpoint}/${id}/disable`).then((response) => response.data); } } diff --git a/app/assets/javascripts/deploy_keys/store/index.js b/app/assets/javascripts/deploy_keys/store/index.js index a350bc99a70..dcd77e921cd 100644 --- a/app/assets/javascripts/deploy_keys/store/index.js +++ b/app/assets/javascripts/deploy_keys/store/index.js @@ -4,6 +4,6 @@ export default class DeployKeysStore { } isEnabled(id) { - return this.keys.enabled_keys.some(key => key.id === id); + return this.keys.enabled_keys.some((key) => key.id === id); } } diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js index fe57dd2dc8f..99351231520 100644 --- a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js +++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js @@ -68,7 +68,7 @@ export class GitLabDropdown { this.remote = new GitLabDropdownRemote(this.options.data, { dataType: this.options.dataType, beforeSend: this.toggleLoading.bind(this), - success: data => { + success: (data) => { this.fullData = data; this.parseData(this.fullData); this.focusTextInput(); @@ -113,7 +113,7 @@ export class GitLabDropdown { return $(selector, this.dropdown); }, data: () => this.fullData, - callback: data => { + callback: (data) => { this.parseData(data); if (this.filterInput.val() !== '') { selector = SELECTABLE_CLASSES; @@ -123,10 +123,7 @@ export class GitLabDropdown { if ($(this.el).is('input')) { currentIndex = -1; } else { - $(selector, this.dropdown) - .first() - .find('a') - .addClass('is-focused'); + $(selector, this.dropdown).first().find('a').addClass('is-focused'); currentIndex = 0; } } @@ -138,13 +135,13 @@ export class GitLabDropdown { this.dropdown.on('hidden.bs.dropdown', this.hidden); $(this.el).on('update.label', this.updateLabel); this.dropdown.on('click', '.dropdown-menu, .dropdown-menu-close', this.shouldPropagate); - this.dropdown.on('keyup', e => { + this.dropdown.on('keyup', (e) => { // Escape key if (e.which === 27) { return $('.dropdown-menu-close', this.dropdown).trigger('click'); } }); - this.dropdown.on('blur', 'a', e => { + this.dropdown.on('blur', 'a', (e) => { let $dropdownMenu; let $relatedTarget; if (e.relatedTarget != null) { @@ -156,7 +153,7 @@ export class GitLabDropdown { } }); if (this.dropdown.find('.dropdown-toggle-page').length) { - this.dropdown.find('.dropdown-toggle-page, .dropdown-menu-back').on('click', e => { + this.dropdown.find('.dropdown-toggle-page, .dropdown-menu-back').on('click', (e) => { e.preventDefault(); e.stopPropagation(); return this.togglePage(); @@ -167,7 +164,7 @@ export class GitLabDropdown { if (this.dropdown.find('.dropdown-toggle-page').length) { selector = '.dropdown-page-one .dropdown-content a'; } - this.dropdown.on('click', selector, e => { + this.dropdown.on('click', selector, (e) => { const $el = $(e.currentTarget); const selected = self.rowClicked($el); const selectedObj = selected ? selected[0] : null; @@ -224,7 +221,7 @@ export class GitLabDropdown { else if (isObject(data)) { html = []; - Object.keys(data).forEach(name => { + Object.keys(data).forEach((name) => { groupData = data[name]; html.push( this.renderItem( @@ -235,7 +232,7 @@ export class GitLabDropdown { name, ), ); - this.renderData(groupData, name).map(item => html.push(item)); + this.renderData(groupData, name).map((item) => html.push(item)); }); } else { // Render each row @@ -274,7 +271,7 @@ export class GitLabDropdown { filteredFullData() { return this.fullData.filter( - r => + (r) => typeof r === 'object' && !Object.prototype.hasOwnProperty.call(r, 'beforeDivider') && !Object.prototype.hasOwnProperty.call(r, 'header'), @@ -392,7 +389,7 @@ export class GitLabDropdown { ...this.options, icon: this.icon, highlight: this.highlight, - highlightText: text => this.highlightTextMatches(text, this.filterInput.val()), + highlightText: (text) => this.highlightTextMatches(text, this.filterInput.val()), highlightTemplate: this.highlightTemplate.bind(this), parent, }, @@ -493,10 +490,7 @@ export class GitLabDropdown { if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) { this.dropdown.find(`.${ACTIVE_CLASS}`).removeClass(ACTIVE_CLASS); if (!isInput) { - this.dropdown - .parent() - .find(`input[name='${fieldName}']`) - .remove(); + this.dropdown.parent().find(`input[name='${fieldName}']`).remove(); } } if (field && field.length && value == null) { @@ -536,16 +530,13 @@ export class GitLabDropdown { $(`input[name="${fieldName}"]`).remove(); } - const $input = $('<input>') - .attr('type', 'hidden') - .attr('name', fieldName) - .val(value); + const $input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value); if (this.options.inputId != null) { $input.attr('id', this.options.inputId); } if (this.options.multiSelect) { - Object.keys(selectedObject).forEach(attribute => { + Object.keys(selectedObject).forEach((attribute) => { $input.attr(`data-${attribute}`, selectedObject[attribute]); }); } @@ -586,7 +577,7 @@ export class GitLabDropdown { if (this.dropdown.find('.dropdown-toggle-page').length) { selector = `.dropdown-page-one ${selector}`; } - return $('body').on('keydown', e => { + return $('body').on('keydown', (e) => { let $listItems; let PREV_INDEX; const currentKeyCode = e.which; @@ -678,9 +669,7 @@ export class GitLabDropdown { toggleText = this.options.updateLabel; } - return $(this.el) - .find('.dropdown-toggle-text') - .text(toggleText); + return $(this.el).find('.dropdown-toggle-text').text(toggleText); } // eslint-disable-next-line class-methods-use-this diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js index 89ffb5f5f79..ab9fb1ec332 100644 --- a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js +++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js @@ -18,19 +18,16 @@ export class GitLabDropdownFilter { this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true; const $inputContainer = this.input.parent(); const $clearButton = $inputContainer.find('.js-dropdown-input-clear'); - $clearButton.on('click', e => { + $clearButton.on('click', (e) => { // Clear click e.preventDefault(); e.stopPropagation(); - return this.input - .val('') - .trigger('input') - .focus(); + return this.input.val('').trigger('input').focus(); }); // Key events timeout = ''; this.input - .on('keydown', e => { + .on('keydown', (e) => { const keyCode = e.which; if (keyCode === 13 && !options.elIsInput) { e.preventDefault(); @@ -49,7 +46,7 @@ export class GitLabDropdownFilter { return (timeout = setTimeout(() => { $inputContainer.parent().addClass('is-loading'); - return this.options.query(this.input.val(), data => { + return this.options.query(this.input.val(), (data) => { $inputContainer.parent().removeClass('is-loading'); return this.options.callback(data); }); @@ -97,13 +94,13 @@ export class GitLabDropdownFilter { // } else if (isObject(data)) { results = {}; - Object.keys(data).forEach(key => { + Object.keys(data).forEach((key) => { group = data[key]; tmp = fuzzaldrinPlus.filter(group, searchText, { key: this.options.keys, }); if (tmp.length) { - results[key] = tmp.map(item => item); + results[key] = tmp.map((item) => item); } }); } @@ -113,7 +110,7 @@ export class GitLabDropdownFilter { const elements = this.options.elements(); if (searchText) { // eslint-disable-next-line func-names - elements.each(function() { + elements.each(function () { const $el = $(this); const matches = fuzzaldrinPlus.match($el.text().trim(), searchText); if (!$el.is('.dropdown-header')) { diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_input.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_input.js index d857071d05f..61d3e5fbf38 100644 --- a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_input.js +++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_input.js @@ -5,24 +5,21 @@ export class GitLabDropdownInput { this.fieldName = this.options.fieldName || 'field-name'; const $inputContainer = this.input.parent(); const $clearButton = $inputContainer.find('.js-dropdown-input-clear'); - $clearButton.on('click', e => { + $clearButton.on('click', (e) => { // Clear click e.preventDefault(); e.stopPropagation(); - return this.input - .val('') - .trigger('input') - .focus(); + return this.input.val('').trigger('input').focus(); }); this.input - .on('keydown', e => { + .on('keydown', (e) => { const keyCode = e.which; if (keyCode === 13 && !options.elIsInput) { e.preventDefault(); } }) - .on('input', e => { + .on('input', (e) => { let val = e.currentTarget.value || this.options.inputFieldName; val = val .split(' ') @@ -31,10 +28,7 @@ export class GitLabDropdownInput { .toLowerCase() // replace non alphanumeric .replace(/(-)\1+/g, '-'); // replace repeated dashes this.cb(this.options.fieldName, val, {}, true); - this.input - .closest('.dropdown') - .find('.dropdown-toggle-text') - .text(val); + this.input.closest('.dropdown').find('.dropdown-toggle-text').text(val); }); } diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js index 1f6a2e1f646..ae5d3298b62 100644 --- a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js +++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js @@ -15,7 +15,7 @@ export class GitLabDropdownRemote { if (this.options.beforeSend) { this.options.beforeSend(); } - return this.dataEndpoint('', data => { + return this.dataEndpoint('', (data) => { // Fetch the data by calling the data function if (this.options.success) { this.options.success(data); diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/index.js b/app/assets/javascripts/deprecated_jquery_dropdown/index.js index 90e7f15b5b7..6a3d2026192 100644 --- a/app/assets/javascripts/deprecated_jquery_dropdown/index.js +++ b/app/assets/javascripts/deprecated_jquery_dropdown/index.js @@ -3,7 +3,7 @@ import { GitLabDropdown } from './gl_dropdown'; export default function initDeprecatedJQueryDropdown($el, opts) { // eslint-disable-next-line func-names - return $el.each(function() { + return $el.each(function () { if (!$.data(this, 'deprecatedJQueryDropdown')) { $.data(this, 'deprecatedJQueryDropdown', new GitLabDropdown(this, opts)); } diff --git a/app/assets/javascripts/design_management/components/design_destroyer.vue b/app/assets/javascripts/design_management/components/design_destroyer.vue index 5d32bfd4a73..01f9cac456d 100644 --- a/app/assets/javascripts/design_management/components/design_destroyer.vue +++ b/app/assets/javascripts/design_management/components/design_destroyer.vue @@ -8,12 +8,6 @@ export default { components: { ApolloMutation, }, - props: { - filenames: { - type: Array, - required: true, - }, - }, inject: { projectPath: { default: '', @@ -23,6 +17,12 @@ export default { defaut: '', }, }, + props: { + filenames: { + type: Array, + required: true, + }, + }, computed: { projectQueryBody() { return { @@ -32,12 +32,7 @@ export default { }, }, methods: { - updateStoreAfterDelete( - store, - { - data: { designManagementDelete }, - }, - ) { + updateStoreAfterDelete(store, { data: { designManagementDelete } }) { updateStoreAfterDesignsDelete( store, designManagementDelete, diff --git a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue index 6aab4bf423e..ea4d5d7b570 100644 --- a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue +++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue @@ -171,7 +171,7 @@ export default { this.$emit('resolve-discussion-error', data.errors[0]); } }) - .catch(err => { + .catch((err) => { this.$emit('resolve-discussion-error', err); }) .finally(() => { diff --git a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue index 7aaac58a1ce..0cc89440754 100644 --- a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue +++ b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue @@ -2,6 +2,7 @@ import { GlButton, GlModal } from '@gitlab/ui'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import { s__ } from '~/locale'; +import { helpPagePath } from '~/helpers/help_page_helper'; export default { name: 'DesignReplyForm', @@ -60,6 +61,9 @@ export default { ? s__('DesignManagement|Comment') : s__('DesignManagement|Save comment'); }, + markdownDocsPath() { + return helpPagePath('user/markdown'); + }, }, mounted() { this.focusInput(); @@ -89,7 +93,7 @@ export default { :can-attach-file="false" :enable-autocomplete="true" :textarea-value="value" - markdown-docs-path="/help/user/markdown" + :markdown-docs-path="markdownDocsPath" class="bordered-box" > <template #textarea> diff --git a/app/assets/javascripts/design_management/components/design_presentation.vue b/app/assets/javascripts/design_management/components/design_presentation.vue index c4d904e0d91..a760adf8b14 100644 --- a/app/assets/javascripts/design_management/components/design_presentation.vue +++ b/app/assets/javascripts/design_management/components/design_presentation.vue @@ -58,7 +58,7 @@ export default { }, computed: { discussionStartingNotes() { - return this.discussions.map(discussion => ({ + return this.discussions.map((discussion) => ({ ...discussion.notes[0], index: discussion.index, })); diff --git a/app/assets/javascripts/design_management/components/design_sidebar.vue b/app/assets/javascripts/design_management/components/design_sidebar.vue index 41dcec38abe..50b12fd739b 100644 --- a/app/assets/javascripts/design_management/components/design_sidebar.vue +++ b/app/assets/javascripts/design_management/components/design_sidebar.vue @@ -21,6 +21,14 @@ export default { DesignTodoButton, }, mixins: [glFeatureFlagsMixin()], + inject: { + projectPath: { + default: '', + }, + issueIid: { + default: '', + }, + }, props: { design: { type: Object, @@ -41,14 +49,6 @@ export default { discussionWithOpenForm: '', }; }, - inject: { - projectPath: { - default: '', - }, - issueIid: { - default: '', - }, - }, computed: { discussions() { return extractDiscussions(this.design.discussions); @@ -63,10 +63,10 @@ export default { return extractParticipants(this.issue.participants.nodes); }, resolvedDiscussions() { - return this.discussions.filter(discussion => discussion.resolved); + return this.discussions.filter((discussion) => discussion.resolved); }, unresolvedDiscussions() { - return this.discussions.filter(discussion => !discussion.resolved); + return this.discussions.filter((discussion) => !discussion.resolved); }, resolvedCommentsToggleIcon() { return this.resolvedDiscussionsExpanded ? 'chevron-down' : 'chevron-right'; diff --git a/app/assets/javascripts/design_management/components/design_todo_button.vue b/app/assets/javascripts/design_management/components/design_todo_button.vue index aff4f348d15..db14db79989 100644 --- a/app/assets/javascripts/design_management/components/design_todo_button.vue +++ b/app/assets/javascripts/design_management/components/design_todo_button.vue @@ -13,12 +13,6 @@ export default { TodoButton, }, mixins: [allVersionsMixin], - props: { - design: { - type: Object, - required: true, - }, - }, inject: { projectPath: { default: '', @@ -27,6 +21,12 @@ export default { default: '', }, }, + props: { + design: { + type: Object, + required: true, + }, + }, data() { return { todoLoading: false, @@ -95,7 +95,7 @@ export default { .then(() => { this.incrementGlobalTodoCount(); }) - .catch(err => { + .catch((err) => { this.$emit('error', Error(CREATE_DESIGN_TODO_ERROR)); throw err; }) @@ -116,12 +116,7 @@ export default { variables: { id, }, - update( - store, - { - data: { todoMarkDone }, - }, - ) { + update(store, { data: { todoMarkDone } }) { const todoMarkDoneFirstError = (todoMarkDone.errors || [])[0]; if (todoMarkDoneFirstError) { this.$emit('error', Error(todoMarkDoneFirstError)); @@ -138,7 +133,7 @@ export default { .then(() => { this.decrementGlobalTodoCount(); }) - .catch(err => { + .catch((err) => { this.$emit('error', Error(DELETE_DESIGN_TODO_ERROR)); throw err; }) diff --git a/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue b/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue index 4edc2e410c7..6091a3183ac 100644 --- a/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue +++ b/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue @@ -26,7 +26,7 @@ export default { return this.designs.length; }, currentIndex() { - return this.designs.findIndex(design => design.filename === this.id); + return this.designs.findIndex((design) => design.filename === this.id); }, paginationText() { return sprintf(s__('DesignManagement|%{current_design} of %{designs_count}'), { diff --git a/app/assets/javascripts/design_management/components/toolbar/index.vue b/app/assets/javascripts/design_management/components/toolbar/index.vue index 4caee863df8..3509a701984 100644 --- a/app/assets/javascripts/design_management/components/toolbar/index.vue +++ b/app/assets/javascripts/design_management/components/toolbar/index.vue @@ -18,6 +18,14 @@ export default { GlTooltip: GlTooltipDirective, }, mixins: [timeagoMixin], + inject: { + projectPath: { + default: '', + }, + issueIid: { + default: '', + }, + }, props: { id: { type: String, @@ -58,14 +66,6 @@ export default { }, }; }, - inject: { - projectPath: { - default: '', - }, - issueIid: { - default: '', - }, - }, apollo: { permissions: { query: permissionsQuery, @@ -75,7 +75,7 @@ export default { iid: this.issueIid, }; }, - update: data => data.project.issue.userPermissions, + update: (data) => data.project.issue.userPermissions, }, }, computed: { diff --git a/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue b/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue index 4a1be7b720a..750f16bbe57 100644 --- a/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue +++ b/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue @@ -19,7 +19,7 @@ export default { if (!this.queryVersion) return 0; const idx = this.allVersions.findIndex( - version => this.findVersionId(version.id) === this.queryVersion, + (version) => this.findVersionId(version.id) === this.queryVersion, ); // if the currentVersionId isn't a valid version (i.e. not in allVersions) diff --git a/app/assets/javascripts/design_management/graphql.js b/app/assets/javascripts/design_management/graphql.js index d1fe977b969..b7aba315168 100644 --- a/app/assets/javascripts/design_management/graphql.js +++ b/app/assets/javascripts/design_management/graphql.js @@ -19,7 +19,7 @@ const resolvers = { updateActiveDiscussion: (_, { id = null, source }, { cache }) => { const sourceData = cache.readQuery({ query: activeDiscussionQuery }); - const data = produce(sourceData, draftData => { + const data = produce(sourceData, (draftData) => { // eslint-disable-next-line no-param-reassign draftData.activeDiscussion = { __typename: 'ActiveDiscussion', @@ -74,7 +74,7 @@ const defaultClient = createDefaultClient( // Should be removed as soon as https://gitlab.com/gitlab-org/gitlab/issues/13495 is resolved { cacheConfig: { - dataIdFromObject: object => { + dataIdFromObject: (object) => { // eslint-disable-next-line no-underscore-dangle, @gitlab/require-i18n-strings if (object.__typename === 'Design') { return object.id && object.image ? `${object.id}-${object.image}` : uniqueId(); diff --git a/app/assets/javascripts/design_management/mixins/all_designs.js b/app/assets/javascripts/design_management/mixins/all_designs.js index 466f61e21fa..4783382d563 100644 --- a/app/assets/javascripts/design_management/mixins/all_designs.js +++ b/app/assets/javascripts/design_management/mixins/all_designs.js @@ -17,7 +17,7 @@ export default { atVersion: this.designsVersion, }; }, - update: data => { + update: (data) => { const designNodes = propertyOf(data)([ 'project', 'issue', diff --git a/app/assets/javascripts/design_management/mixins/all_versions.js b/app/assets/javascripts/design_management/mixins/all_versions.js index 07cd0fc92bd..42cef35a08e 100644 --- a/app/assets/javascripts/design_management/mixins/all_versions.js +++ b/app/assets/javascripts/design_management/mixins/all_versions.js @@ -12,7 +12,7 @@ export default { atVersion: null, }; }, - update: data => data.project.issue.designCollection.versions.nodes, + update: (data) => data.project.issue.designCollection.versions.nodes, }, }, inject: { @@ -28,7 +28,7 @@ export default { return ( this.$route.query.version && this.allVersions && - this.allVersions.some(version => version.id.endsWith(this.$route.query.version)) + this.allVersions.some((version) => version.id.endsWith(this.$route.query.version)) ); }, designsVersion() { diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue index fb86568c304..492ed2e8719 100644 --- a/app/assets/javascripts/design_management/pages/design/index.vue +++ b/app/assets/javascripts/design_management/pages/design/index.vue @@ -57,6 +57,27 @@ export default { DesignSidebar, }, mixins: [allVersionsMixin, glFeatureFlagsMixin()], + beforeRouteUpdate(to, from, next) { + // reset scale when the active design changes + this.scale = DEFAULT_SCALE; + next(); + }, + beforeRouteEnter(to, from, next) { + const pageEl = getPageLayoutElement(); + if (pageEl) { + pageEl.classList.add(...DESIGN_DETAIL_LAYOUT_CLASSLIST); + } + + next(); + }, + beforeRouteLeave(to, from, next) { + const pageEl = getPageLayoutElement(); + if (pageEl) { + pageEl.classList.remove(...DESIGN_DETAIL_LAYOUT_CLASSLIST); + } + + next(); + }, props: { id: { type: String, @@ -81,7 +102,7 @@ export default { variables() { return this.designVariables; }, - update: data => extractDesign(data), + update: (data) => extractDesign(data), result(res) { this.onDesignQueryResult(res); }, @@ -139,7 +160,7 @@ export default { return Boolean(this.annotationCoordinates); }, resolvedDiscussions() { - return this.discussions.filter(discussion => discussion.resolved); + return this.discussions.filter((discussion) => discussion.resolved); }, }, watch: { @@ -161,18 +182,8 @@ export default { beforeDestroy() { Mousetrap.unbind('esc', this.closeDesign); }, - beforeRouteUpdate(to, from, next) { - // reset scale when the active design changes - this.scale = DEFAULT_SCALE; - next(); - }, methods: { - addImageDiffNoteToStore( - store, - { - data: { createImageDiffNote }, - }, - ) { + addImageDiffNoteToStore(store, { data: { createImageDiffNote } }) { updateStoreAfterAddImageDiffNote( store, createImageDiffNote, @@ -180,12 +191,7 @@ export default { this.designVariables, ); }, - updateImageDiffNoteInStore( - store, - { - data: { repositionImageDiffNote }, - }, - ) { + updateImageDiffNoteInStore(store, { data: { repositionImageDiffNote } }) { return updateStoreAfterRepositionImageDiffNote( store, repositionImageDiffNote, @@ -213,7 +219,7 @@ export default { update: this.updateImageDiffNoteInStore, }; - return this.$apollo.mutate(mutationPayload).catch(e => this.onUpdateImageDiffNoteError(e)); + return this.$apollo.mutate(mutationPayload).catch((e) => this.onUpdateImageDiffNoteError(e)); }, onDesignQueryResult({ data, loading }) { // On the initial load with cache-and-network policy data is undefined while loading is true @@ -306,22 +312,6 @@ export default { this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded; }, }, - beforeRouteEnter(to, from, next) { - const pageEl = getPageLayoutElement(); - if (pageEl) { - pageEl.classList.add(...DESIGN_DETAIL_LAYOUT_CLASSLIST); - } - - next(); - }, - beforeRouteLeave(to, from, next) { - const pageEl = getPageLayoutElement(); - if (pageEl) { - pageEl.classList.remove(...DESIGN_DETAIL_LAYOUT_CLASSLIST); - } - - next(); - }, createImageDiffNoteMutation, DESIGNS_ROUTE_NAME, }; diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue index ea404692840..5c82a7331b6 100644 --- a/app/assets/javascripts/design_management/pages/index.vue +++ b/app/assets/javascripts/design_management/pages/index.vue @@ -69,9 +69,13 @@ export default { iid: this.issueIid, }; }, - update: data => data.project.issue.userPermissions, + update: (data) => data.project.issue.userPermissions, }, }, + beforeRouteUpdate(to, from, next) { + this.selectedDesigns = []; + next(); + }, data() { return { permissions: { @@ -184,15 +188,10 @@ export default { return this.$apollo .mutate(mutationPayload) - .then(res => this.onUploadDesignDone(res)) + .then((res) => this.onUploadDesignDone(res)) .catch(() => this.onUploadDesignError()); }, - afterUploadDesign( - store, - { - data: { designManagementUpload }, - }, - ) { + afterUploadDesign(store, { data: { designManagementUpload } }) { updateStoreAfterUploadDesign(store, designManagementUpload, this.projectQueryBody); }, onUploadDesignDone(res) { @@ -213,7 +212,7 @@ export default { this.trackUploadDesign(res); }, trackUploadDesign(res) { - (res?.data?.designManagementUpload?.designs || []).forEach(design => { + (res?.data?.designManagementUpload?.designs || []).forEach((design) => { if (design.event === 'CREATION') { trackDesignCreate(); } else if (design.event === 'MODIFICATION') { @@ -227,7 +226,7 @@ export default { }, changeSelectedDesigns(filename) { if (this.isDesignSelected(filename)) { - this.selectedDesigns = this.selectedDesigns.filter(design => design !== filename); + this.selectedDesigns = this.selectedDesigns.filter((design) => design !== filename); } else { this.selectedDesigns.push(filename); } @@ -236,14 +235,14 @@ export default { if (this.hasSelectedDesigns) { this.selectedDesigns = []; } else { - this.selectedDesigns = this.designs.map(design => design.filename); + this.selectedDesigns = this.designs.map((design) => design.filename); } }, isDesignSelected(filename) { return this.selectedDesigns.includes(filename); }, isDesignToBeSaved(filename) { - return this.filesToBeSaved.some(file => file.name === filename); + return this.filesToBeSaved.some((file) => file.name === filename); }, canSelectDesign(filename) { return this.isLatestVersion && this.canCreateDesign && !this.isDesignToBeSaved(filename); @@ -329,10 +328,6 @@ export default { this.reorderedDesigns = designs; }, }, - beforeRouteUpdate(to, from, next) { - this.selectedDesigns = []; - next(); - }, dragOptions: { animation: 200, ghostClass: 'gl-visibility-hidden', diff --git a/app/assets/javascripts/design_management/router/routes.js b/app/assets/javascripts/design_management/router/routes.js index d888b856611..1b07d8aeb76 100644 --- a/app/assets/javascripts/design_management/router/routes.js +++ b/app/assets/javascripts/design_management/router/routes.js @@ -13,13 +13,7 @@ export default [ name: DESIGN_ROUTE_NAME, path: '/designs/:id', component: DesignDetail, - beforeEnter( - { - params: { id }, - }, - _, - next, - ) { + beforeEnter({ params: { id } }, _, next) { if (typeof id === 'string') { next(); } diff --git a/app/assets/javascripts/design_management/utils/cache_update.js b/app/assets/javascripts/design_management/utils/cache_update.js index 5bd0288d037..0c4ee0bf012 100644 --- a/app/assets/javascripts/design_management/utils/cache_update.js +++ b/app/assets/javascripts/design_management/utils/cache_update.js @@ -11,14 +11,14 @@ import { designDeletionError, } from './error_messages'; -const designsOf = data => data.project.issue.designCollection.designs; +const designsOf = (data) => data.project.issue.designCollection.designs; const deleteDesignsFromStore = (store, query, selectedDesigns) => { const sourceData = store.readQuery(query); - const data = produce(sourceData, draftData => { + const data = produce(sourceData, (draftData) => { const changedDesigns = designsOf(sourceData).nodes.filter( - design => !selectedDesigns.includes(design.filename), + (design) => !selectedDesigns.includes(design.filename), ); designsOf(draftData).nodes = [...changedDesigns]; }); @@ -40,7 +40,7 @@ const addNewVersionToStore = (store, query, version) => { if (!version) return; const sourceData = store.readQuery(query); - const data = produce(sourceData, draftData => { + const data = produce(sourceData, (draftData) => { // eslint-disable-next-line no-param-reassign draftData.project.issue.designCollection.versions.nodes = [ version, @@ -74,14 +74,14 @@ const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) = }, }; - const data = produce(sourceData, draftData => { + const data = produce(sourceData, (draftData) => { const design = extractDesign(draftData); design.notesCount += 1; design.discussions.nodes = [...design.discussions.nodes, newDiscussion]; if ( !design.issue.participants.nodes.some( - participant => participant.username === createImageDiffNote.note.author.username, + (participant) => participant.username === createImageDiffNote.note.author.username, ) ) { design.issue.participants.nodes = [ @@ -107,7 +107,7 @@ const updateImageDiffNoteInStore = (store, repositionImageDiffNote, query, varia variables, }); - const data = produce(sourceData, draftData => { + const data = produce(sourceData, (draftData) => { const design = extractDesign(draftData); const discussion = extractCurrentDiscussion( design.discussions, @@ -130,18 +130,18 @@ const updateImageDiffNoteInStore = (store, repositionImageDiffNote, query, varia const addNewDesignToStore = (store, designManagementUpload, query) => { const sourceData = store.readQuery(query); - const data = produce(sourceData, draftData => { + const data = produce(sourceData, (draftData) => { const currentDesigns = extractDesigns(draftData); const difference = differenceBy(designManagementUpload.designs, currentDesigns, 'filename'); const newDesigns = currentDesigns - .map(design => { - return designManagementUpload.designs.find(d => d.filename === design.filename) || design; + .map((design) => { + return designManagementUpload.designs.find((d) => d.filename === design.filename) || design; }) .concat(difference); let newVersionNode; - const findNewVersions = designManagementUpload.designs.find(design => design.versions); + const findNewVersions = designManagementUpload.designs.find((design) => design.versions); if (findNewVersions) { const findNewVersionsNodes = findNewVersions.versions.nodes; @@ -181,7 +181,7 @@ const addNewDesignToStore = (store, designManagementUpload, query) => { const moveDesignInStore = (store, designManagementMove, query) => { const sourceData = store.readQuery(query); - const data = produce(sourceData, draftData => { + const data = produce(sourceData, (draftData) => { // eslint-disable-next-line no-param-reassign draftData.project.issue.designCollection.designs = designManagementMove.designCollection.designs; @@ -199,7 +199,7 @@ export const addPendingTodoToStore = (store, pendingTodo, query, queryVariables) variables: queryVariables, }); - const data = produce(sourceData, draftData => { + const data = produce(sourceData, (draftData) => { const design = extractDesign(draftData); const existingTodos = design.currentUserTodos?.nodes || []; const newTodoNodes = [...existingTodos, { ...pendingTodo, __typename: 'Todo' }]; @@ -226,7 +226,7 @@ export const deletePendingTodoFromStore = (store, todoMarkDone, query, queryVari const { todo: { id: todoId }, } = todoMarkDone; - const data = produce(sourceData, draftData => { + const data = produce(sourceData, (draftData) => { const design = extractDesign(draftData); const existingTodos = design.currentUserTodos?.nodes || []; diff --git a/app/assets/javascripts/design_management/utils/design_management_utils.js b/app/assets/javascripts/design_management/utils/design_management_utils.js index a905230811c..05b220801f2 100644 --- a/app/assets/javascripts/design_management/utils/design_management_utils.js +++ b/app/assets/javascripts/design_management/utils/design_management_utils.js @@ -10,7 +10,7 @@ export const isValidDesignFile = ({ type }) => * @param {Array} discussions */ -export const extractDiscussions = discussions => +export const extractDiscussions = (discussions) => discussions.nodes.map((discussion, index) => ({ ...discussion, index: index + 1, @@ -24,27 +24,27 @@ export const extractDiscussions = discussions => */ export const extractCurrentDiscussion = (discussions, id) => - discussions.nodes.find(discussion => discussion.id === id); + discussions.nodes.find((discussion) => discussion.id === id); -export const findVersionId = id => (id.match('::Version/(.+$)') || [])[1]; +export const findVersionId = (id) => (id.match('::Version/(.+$)') || [])[1]; -export const findNoteId = id => (id.match('DiffNote/(.+$)') || [])[1]; +export const findNoteId = (id) => (id.match('DiffNote/(.+$)') || [])[1]; -export const findIssueId = id => (id.match('Issue/(.+$)') || [])[1]; +export const findIssueId = (id) => (id.match('Issue/(.+$)') || [])[1]; -export const findDesignId = id => (id.match('Design/(.+$)') || [])[1]; +export const findDesignId = (id) => (id.match('Design/(.+$)') || [])[1]; -export const extractDesigns = data => data.project.issue.designCollection.designs.nodes; +export const extractDesigns = (data) => data.project.issue.designCollection.designs.nodes; -export const extractDesign = data => (extractDesigns(data) || [])[0]; +export const extractDesign = (data) => (extractDesigns(data) || [])[0]; -export const toDiffNoteGid = noteId => `gid://gitlab/DiffNote/${noteId}`; +export const toDiffNoteGid = (noteId) => `gid://gitlab/DiffNote/${noteId}`; /** * Return the note ID from a URL hash parameter * @param {String} urlHash URL hash, including `#` prefix */ -export const extractDesignNoteId = urlHash => { +export const extractDesignNoteId = (urlHash) => { const [, noteId] = urlHash.match('#note_([0-9]+$)') || []; return noteId || null; }; @@ -53,8 +53,8 @@ export const extractDesignNoteId = urlHash => { * Generates optimistic response for a design upload mutation * @param {Array<File>} files */ -export const designUploadOptimisticResponse = files => { - const designs = files.map(file => ({ +export const designUploadOptimisticResponse = (files) => { + const designs = files.map((file) => ({ // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26 // eslint-disable-next-line @gitlab/require-i18n-strings __typename: 'Design', @@ -128,7 +128,7 @@ export const repositionImageDiffNoteOptimisticResponse = (note, { position }) => * Generates optimistic response for a design upload mutation * @param {Array} designs */ -export const moveDesignOptimisticResponse = designs => ({ +export const moveDesignOptimisticResponse = (designs) => ({ // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26 // eslint-disable-next-line @gitlab/require-i18n-strings __typename: 'Mutation', @@ -145,13 +145,13 @@ export const moveDesignOptimisticResponse = designs => ({ }, }); -const normalizeAuthor = author => ({ +const normalizeAuthor = (author) => ({ ...author, web_url: author.webUrl, avatar_url: author.avatarUrl, }); -export const extractParticipants = users => users.map(node => normalizeAuthor(node)); +export const extractParticipants = (users) => users.map((node) => normalizeAuthor(node)); export const getPageLayoutElement = () => document.querySelector('.layout-page'); @@ -160,14 +160,14 @@ export const getPageLayoutElement = () => document.querySelector('.layout-page') * Example of todoDeletePath: /delete/1234 * @param {String} todoDeletePath delete_path from REST API response */ -export const extractTodoIdFromDeletePath = todoDeletePath => +export const extractTodoIdFromDeletePath = (todoDeletePath) => (todoDeletePath.match('todos/([0-9]+$)') || [])[1]; -const createTodoGid = todoId => { +const createTodoGid = (todoId) => { return `gid://gitlab/Todo/${todoId}`; }; -export const createPendingTodo = todoId => { +export const createPendingTodo = (todoId) => { return { __typename: 'Todo', // eslint-disable-line @gitlab/require-i18n-strings id: createTodoGid(todoId), diff --git a/app/assets/javascripts/design_management/utils/error_messages.js b/app/assets/javascripts/design_management/utils/error_messages.js index bd21d711462..cb4bb6e26a8 100644 --- a/app/assets/javascripts/design_management/utils/error_messages.js +++ b/app/assets/javascripts/design_management/utils/error_messages.js @@ -54,7 +54,7 @@ export const TOGGLE_TODO_ERROR = __('Failed to toggle To-Do for the design.'); const MAX_SKIPPED_FILES_LISTINGS = 5; -const oneDesignSkippedMessage = filename => +const oneDesignSkippedMessage = (filename) => `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${sprintf(s__('DesignManagement|%{filename} did not change.'), { filename, })}`; @@ -64,7 +64,7 @@ const oneDesignSkippedMessage = filename => * files were skipped. * @param {Array<{ filename }>} skippedFiles */ -const someDesignsSkippedMessage = skippedFiles => { +const someDesignsSkippedMessage = (skippedFiles) => { const designsSkippedMessage = `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${s__( 'Some of the designs you tried uploading did not change:', )}`; diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index 9497ea7bb4f..c4e86638e9d 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -24,9 +24,7 @@ export default class Diff { if (!tab || (tab && tab.dataset && tab.dataset.isLocked !== '')) FilesCommentButton.init($diffFile); - const firstFile = $('.files') - .first() - .get(0); + const firstFile = $('.files').first().get(0); const canCreateNote = firstFile && firstFile.hasAttribute('data-can-create-note'); $diffFile.each((index, file) => initImageDiffHelper.initImageDiff(file, canCreateNote)); @@ -119,7 +117,7 @@ export default class Diff { table.removeClass('left-side-selected right-side-selected'); - const lineClass = ['left-side', 'right-side'].filter(name => line.hasClass(name))[0]; + const lineClass = ['left-side', 'right-side'].filter((name) => line.hasClass(name))[0]; if (lineClass) { table.addClass(`${lineClass}-selected`); } @@ -134,7 +132,7 @@ export default class Diff { if (children.length !== 2) { return [0, 0]; } - return children.map(elm => parseInt($(elm).data('linenumber'), 10) || 0); + return children.map((elm) => parseInt($(elm).data('linenumber'), 10) || 0); } // eslint-disable-next-line class-methods-use-this highlightSelectedLine() { diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 7827c78b658..32822fe1fe8 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -124,6 +124,16 @@ export default { required: false, default: false, }, + defaultSuggestionCommitMessage: { + type: String, + required: false, + default: '', + }, + mrReviews: { + type: Object, + required: false, + default: () => ({}), + }, }, data() { const treeWidth = @@ -136,19 +146,17 @@ export default { }, computed: { ...mapState({ - isLoading: state => state.diffs.isLoading, - isBatchLoading: state => state.diffs.isBatchLoading, - diffFiles: state => state.diffs.diffFiles, - diffViewType: state => state.diffs.diffViewType, - mergeRequestDiffs: state => state.diffs.mergeRequestDiffs, - mergeRequestDiff: state => state.diffs.mergeRequestDiff, - commit: state => state.diffs.commit, - renderOverflowWarning: state => state.diffs.renderOverflowWarning, - numTotalFiles: state => state.diffs.realSize, - numVisibleFiles: state => state.diffs.size, - plainDiffPath: state => state.diffs.plainDiffPath, - emailPatchPath: state => state.diffs.emailPatchPath, - retrievingBatches: state => state.diffs.retrievingBatches, + isLoading: (state) => state.diffs.isLoading, + isBatchLoading: (state) => state.diffs.isBatchLoading, + diffFiles: (state) => state.diffs.diffFiles, + diffViewType: (state) => state.diffs.diffViewType, + commit: (state) => state.diffs.commit, + renderOverflowWarning: (state) => state.diffs.renderOverflowWarning, + numTotalFiles: (state) => state.diffs.realSize, + numVisibleFiles: (state) => state.diffs.size, + plainDiffPath: (state) => state.diffs.plainDiffPath, + emailPatchPath: (state) => state.diffs.emailPatchPath, + retrievingBatches: (state) => state.diffs.retrievingBatches, }), ...mapState('diffs', [ 'showTreeList', @@ -161,7 +169,12 @@ export default { 'hasConflicts', 'viewDiffsFileByFile', ]), - ...mapGetters('diffs', ['whichCollapsedTypes', 'isParallelView', 'currentDiffIndex']), + ...mapGetters('diffs', [ + 'whichCollapsedTypes', + 'isParallelView', + 'currentDiffIndex', + 'fileReviews', + ]), ...mapGetters(['isNotesFetched', 'getNoteableData']), diffs() { if (!this.viewDiffsFileByFile) { @@ -176,17 +189,16 @@ export default { return this.currentUser.can_fork === true && this.currentUser.can_create_merge_request; }, renderDiffFiles() { - return ( - this.diffFiles.length > 0 || - (this.startVersion && - this.startVersion.version_index === this.mergeRequestDiff.version_index) - ); + return this.diffFiles.length > 0; + }, + renderFileTree() { + return this.renderDiffFiles && this.showTreeList; }, hideFileStats() { return this.treeWidth <= TREE_HIDE_STATS_WIDTH; }, isLimitedContainer() { - return !this.showTreeList && !this.isParallelView && !this.isFluidLayout; + return !this.renderFileTree && !this.isParallelView && !this.isFluidLayout; }, isDiffHead() { return parseBoolean(getParameterByName('diff_head')); @@ -249,7 +261,7 @@ export default { this.adjustView(); }, isLoading: 'adjustView', - showTreeList: 'adjustView', + renderFileTree: 'adjustView', }, mounted() { this.setBaseConfig({ @@ -261,6 +273,8 @@ export default { dismissEndpoint: this.dismissEndpoint, showSuggestPopover: this.showSuggestPopover, viewDiffsFileByFile: fileByFile(this.fileByFileUserPreference), + defaultSuggestionCommitMessage: this.defaultSuggestionCommitMessage, + mrReviews: this.mrReviews || {}, }); if (this.shouldShow) { @@ -270,12 +284,7 @@ export default { const id = window?.location?.hash; if (id && id.indexOf('#note') !== 0) { - this.setHighlightedRow( - id - .split('diff-content') - .pop() - .slice(1), - ); + this.setHighlightedRow(id.split('diff-content').pop().slice(1)); } }, beforeCreate() { @@ -393,10 +402,7 @@ export default { }, setDiscussions() { requestIdleCallback( - () => - this.assignDiscussionsToDiff() - .then(this.$nextTick) - .then(this.startTaskList), + () => this.assignDiscussionsToDiff().then(this.$nextTick).then(this.startTaskList), { timeout: 1000 }, ); }, @@ -425,7 +431,7 @@ export default { } }); - if (this.commit && this.glFeatures.mrCommitNeighborNav) { + if (this.commit) { Mousetrap.bind('c', () => this.moveToNeighboringCommit({ direction: 'next' })); Mousetrap.bind('x', () => this.moveToNeighboringCommit({ direction: 'previous' })); } @@ -464,7 +470,6 @@ export default { <div v-if="isLoading || !isTreeLoaded" class="loading"><gl-loading-icon size="lg" /></div> <div v-else id="diffs" :class="{ active: shouldShow }" class="diffs tab-pane"> <compare-versions - :merge-request-diffs="mergeRequestDiffs" :is-limited-container="isLimitedContainer" :diff-files-count-text="numTotalFiles" /> @@ -492,7 +497,7 @@ export default { class="files d-flex gl-mt-2" > <div - v-if="showTreeList" + v-if="renderFileTree" :style="{ width: `${treeWidth}px` }" class="diff-tree-list js-diff-tree-list px-3 pr-md-0" > @@ -519,6 +524,7 @@ export default { v-for="(file, index) in diffs" :key="file.newPath" :file="file" + :reviewed="fileReviews[index]" :is-first-file="index === 0" :is-last-file="index === diffs.length - 1" :help-page-path="helpPagePath" diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue index a548354f257..af19f90ee77 100644 --- a/app/assets/javascripts/diffs/components/commit_item.vue +++ b/app/assets/javascripts/diffs/components/commit_item.vue @@ -6,7 +6,7 @@ import { GlButtonGroup, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui' import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; -import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; @@ -39,7 +39,7 @@ import { setUrlParams } from '../../lib/utils/url_utility'; export default { components: { UserAvatarLink, - ClipboardButton, + ModalCopyButton, TimeAgoTooltip, CommitPipelineStatus, GlButtonGroup, @@ -142,16 +142,13 @@ export default { data-testid="commit-sha-short-id" v-text="commit.short_id" /> - <clipboard-button + <modal-copy-button :text="commit.id" :title="__('Copy commit SHA')" class="input-group-text" /> </gl-button-group> - <div - v-if="hasNeighborCommits && glFeatures.mrCommitNeighborNav" - class="commit-nav-buttons ml-3" - > + <div v-if="hasNeighborCommits" class="commit-nav-buttons ml-3"> <gl-button-group> <gl-button :href="previousCommitUrl" diff --git a/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue index da34a7ee19b..2c249f71091 100644 --- a/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue +++ b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue @@ -16,7 +16,7 @@ export default { }, computed: { selectedVersionName() { - return this.versions.find(x => x.selected)?.versionName || ''; + return this.versions.find((x) => x.selected)?.versionName || ''; }, }, }; diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue index f3cc359a679..489278fd6ef 100644 --- a/app/assets/javascripts/diffs/components/compare_versions.vue +++ b/app/assets/javascripts/diffs/components/compare_versions.vue @@ -22,10 +22,6 @@ export default { GlTooltip: GlTooltipDirective, }, props: { - mergeRequestDiffs: { - type: Array, - required: true, - }, isLimitedContainer: { type: Boolean, required: false, @@ -44,6 +40,7 @@ export default { 'diffCompareDropdownSourceVersions', ]), ...mapState('diffs', [ + 'diffFiles', 'commit', 'showTreeList', 'startVersion', @@ -51,12 +48,15 @@ export default { 'addedLines', 'removedLines', ]), - showDropdowns() { - return !this.commit && this.mergeRequestDiffs.length; - }, toggleFileBrowserTitle() { return this.showTreeList ? __('Hide file browser') : __('Show file browser'); }, + hasChanges() { + return this.diffFiles.length > 0; + }, + hasSourceVersions() { + return this.diffCompareDropdownSourceVersions.length > 0; + }, }, created() { this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES; @@ -82,6 +82,7 @@ export default { }" > <gl-button + v-if="hasChanges" v-gl-tooltip.hover variant="default" icon="file-tree" @@ -90,8 +91,12 @@ export default { :selected="showTreeList" @click="setShowTreeList({ showTreeList: !showTreeList })" /> + <div v-if="commit"> + {{ __('Viewing commit') }} + <gl-link :href="commit.commit_url" class="monospace">{{ commit.short_id }}</gl-link> + </div> <gl-sprintf - v-if="showDropdowns" + v-else-if="hasSourceVersions" class="d-flex align-items-center compare-versions-container" :message="s__('MergeRequest|Compare %{target} and %{source}')" > @@ -109,11 +114,7 @@ export default { /> </template> </gl-sprintf> - <div v-else-if="commit"> - {{ __('Viewing commit') }} - <gl-link :href="commit.commit_url" class="monospace">{{ commit.short_id }}</gl-link> - </div> - <div class="inline-parallel-buttons d-none d-md-flex ml-auto"> + <div v-if="hasChanges" class="inline-parallel-buttons d-none d-md-flex ml-auto"> <diff-stats :diff-files-count-text="diffFilesCountText" :added-lines="addedLines" diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue index f938ea368d8..f4e2571dd09 100644 --- a/app/assets/javascripts/diffs/components/diff_content.vue +++ b/app/assets/javascripts/diffs/components/diff_content.vue @@ -50,7 +50,7 @@ export default { }, computed: { ...mapState({ - projectPath: state => state.diffs.projectPath, + projectPath: (state) => state.diffs.projectPath, }), ...mapGetters('diffs', [ 'isInlineView', diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue index 2401e12e4f6..2d1a7237122 100644 --- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue +++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue @@ -13,7 +13,7 @@ const EXPAND_DOWN = 2; const lineNumberByViewType = (viewType, diffLine) => { const numberGetters = { - [INLINE_DIFF_VIEW_TYPE]: line => line?.new_line, + [INLINE_DIFF_VIEW_TYPE]: (line) => line?.new_line, }; const numberGetter = numberGetters[viewType]; return numberGetter && numberGetter(diffLine); @@ -56,7 +56,7 @@ export default { }, computed: { ...mapState({ - diffFiles: state => state.diffs.diffFiles, + diffFiles: (state) => state.diffs.diffFiles, }), canExpandUp() { return !this.isBottom; diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index ed94cabe124..e613b684345 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -37,6 +37,11 @@ export default { type: Object, required: true, }, + reviewed: { + type: Boolean, + required: false, + default: false, + }, isFirstFile: { type: Boolean, required: false, @@ -134,7 +139,7 @@ export default { return !this.isCollapsed || this.automaticallyCollapsed; }, showWarning() { - return this.isCollapsed && (this.automaticallyCollapsed && !this.viewDiffsFileByFile); + return this.isCollapsed && this.automaticallyCollapsed && !this.viewDiffsFileByFile; }, showContent() { return !this.isCollapsed && !this.isFileTooLarge; @@ -205,7 +210,7 @@ export default { await this.$nextTick(); - eventsForThisFile.forEach(event => { + eventsForThisFile.forEach((event) => { eventHub.$emit(event); }); }, diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue index 439319f487c..f62b31734c5 100644 --- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue +++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue @@ -29,7 +29,7 @@ export default { return this.discussions.reduce((acc, note) => acc.concat(note.notes), []); }, notesInGutter() { - return this.allDiscussions.slice(0, COUNT_OF_AVATARS_IN_GUTTER).map(n => ({ + return this.allDiscussions.slice(0, COUNT_OF_AVATARS_IN_GUTTER).map((n) => ({ note: n.note, author: n.author, })); diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue index 172a2bdde7d..463b7f5cff4 100644 --- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue +++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue @@ -56,10 +56,11 @@ export default { }, computed: { ...mapState({ - noteableData: state => state.notes.noteableData, - diffViewType: state => state.diffs.diffViewType, + diffViewType: ({ diffs }) => diffs.diffViewType, + showSuggestPopover: ({ diffs }) => diffs.showSuggestPopover, + noteableData: ({ notes }) => notes.noteableData, + selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition, }), - ...mapState('diffs', ['showSuggestPopover']), ...mapGetters('diffs', ['getDiffFileByHash', 'diffLines']), ...mapGetters([ 'isLoggedIn', @@ -126,6 +127,10 @@ export default { this.initAutoSave(this.noteableData, keys); } + + if (this.selectedCommentPosition) { + this.commentLineStart = this.selectedCommentPosition.start; + } }, methods: { ...mapActions('diffs', [ diff --git a/app/assets/javascripts/diffs/components/diff_row.vue b/app/assets/javascripts/diffs/components/diff_row.vue index c0719e2a7d9..db03da966c3 100644 --- a/app/assets/javascripts/diffs/components/diff_row.vue +++ b/app/assets/javascripts/diffs/components/diff_row.vue @@ -1,7 +1,16 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; -import { CONTEXT_LINE_CLASS_NAME, PARALLEL_DIFF_VIEW_TYPE } from '../constants'; +import { + CONTEXT_LINE_CLASS_NAME, + PARALLEL_DIFF_VIEW_TYPE, + CONFLICT_MARKER_OUR, + CONFLICT_MARKER_THEIR, + CONFLICT_OUR, + CONFLICT_THEIR, + CONFLICT_MARKER, +} from '../constants'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import DiffGutterAvatars from './diff_gutter_avatars.vue'; import * as utils from './diff_row_utils'; @@ -14,6 +23,7 @@ export default { GlTooltip: GlTooltipDirective, SafeHtml, }, + mixins: [glFeatureFlagsMixin()], props: { fileHash: { type: String, @@ -37,6 +47,15 @@ export default { required: false, default: false, }, + index: { + type: Number, + required: true, + }, + }, + data() { + return { + dragging: false, + }; }, computed: { ...mapGetters('diffs', ['fileLineCoverage']), @@ -44,26 +63,40 @@ export default { ...mapState({ isHighlighted(state) { const line = this.line.left?.line_code ? this.line.left : this.line.right; - return utils.isHighlighted(state, line, this.isCommented); + return utils.isHighlighted(state, line, false); }, }), classNameMap() { return { [CONTEXT_LINE_CLASS_NAME]: this.line.isContextLineLeft, - [PARALLEL_DIFF_VIEW_TYPE]: true, + [PARALLEL_DIFF_VIEW_TYPE]: !this.inline, + commented: this.isCommented, }; }, parallelViewLeftLineType() { - return utils.parallelViewLeftLineType(this.line, this.isHighlighted); + return utils.parallelViewLeftLineType(this.line, this.isHighlighted || this.isCommented); }, - coverageState() { + coverageStateLeft() { + if (!this.inline || !this.line.left) return {}; + return this.fileLineCoverage(this.filePath, this.line.left.new_line); + }, + coverageStateRight() { + if (!this.line.right) return {}; return this.fileLineCoverage(this.filePath, this.line.right.new_line); }, classNameMapCellLeft() { - return utils.classNameMapCell(this.line.left, this.isHighlighted, this.isLoggedIn); + return utils.classNameMapCell({ + line: this.line.left, + hll: this.isHighlighted || this.isCommented, + isLoggedIn: this.isLoggedIn, + }); }, classNameMapCellRight() { - return utils.classNameMapCell(this.line.right, this.isHighlighted, this.isLoggedIn); + return utils.classNameMapCell({ + line: this.line.right, + hll: this.isHighlighted || this.isCommented, + isLoggedIn: this.isLoggedIn, + }); }, addCommentTooltipLeft() { return utils.addCommentTooltip(this.line.left); @@ -71,6 +104,12 @@ export default { addCommentTooltipRight() { return utils.addCommentTooltip(this.line.right); }, + emptyCellRightClassMap() { + return { conflict_their: this.line.left?.type === CONFLICT_OUR }; + }, + emptyCellLeftClassMap() { + return { conflict_our: this.line.right?.type === CONFLICT_THEIR }; + }, shouldRenderCommentButton() { return ( this.isLoggedIn && @@ -80,6 +119,9 @@ export default { !this.line.hasDiscussionsRight ); }, + isLeftConflictMarker() { + return [CONFLICT_MARKER_OUR, CONFLICT_MARKER_THEIR].includes(this.line.left?.type); + }, }, mounted() { this.scrollToLineIfNeededParallel(this.line); @@ -98,7 +140,9 @@ export default { const table = line.closest('.diff-table'); table.classList.remove('left-side-selected', 'right-side-selected'); - const [lineClass] = ['left-side', 'right-side'].filter(name => line.classList.contains(name)); + const [lineClass] = ['left-side', 'right-side'].filter((name) => + line.classList.contains(name), + ); if (lineClass) { table.classList.add(`${lineClass}-selected`); @@ -107,37 +151,75 @@ export default { handleCommentButton(line) { this.showCommentForm({ lineCode: line.line_code, fileHash: this.fileHash }); }, + conflictText(line) { + return line.type === CONFLICT_MARKER_THEIR + ? this.$options.THEIR_CHANGES + : this.$options.OUR_CHANGES; + }, + onDragEnd() { + this.dragging = false; + if (!this.glFeatures.dragCommentSelection) return; + + this.$emit('stopdragging'); + }, + onDragEnter(line, index) { + if (!this.glFeatures.dragCommentSelection) return; + + this.$emit('enterdragging', { ...line, index }); + }, + onDragStart(line) { + this.$root.$emit('bv::hide::tooltip'); + this.dragging = true; + this.$emit('startdragging', line); + }, }, + OUR_CHANGES: 'HEAD//our changes', + THEIR_CHANGES: 'origin//their changes', + CONFLICT_MARKER, + CONFLICT_MARKER_THEIR, + CONFLICT_OUR, + CONFLICT_THEIR, }; </script> <template> <div :class="classNameMap" class="diff-grid-row diff-tr line_holder"> - <div class="diff-grid-left left-side"> - <template v-if="line.left"> + <div + data-testid="left-side" + class="diff-grid-left left-side" + @dragover.prevent + @dragenter="onDragEnter(line.left, index)" + @dragend="onDragEnd" + > + <template v-if="line.left && line.left.type !== $options.CONFLICT_MARKER"> <div :class="classNameMapCellLeft" data-testid="leftLineNumber" - class="diff-td diff-line-num old_line" + class="diff-td diff-line-num" > - <span - v-if="shouldRenderCommentButton" - v-gl-tooltip - data-testid="leftCommentButton" - class="add-diff-note tooltip-wrapper" - :title="addCommentTooltipLeft" - > - <button - type="button" - class="add-diff-note note-button js-add-diff-note-button qa-diff-comment" - :disabled="line.left.commentsDisabled" - @click="handleCommentButton(line.left)" + <template v-if="!isLeftConflictMarker"> + <span + v-if="shouldRenderCommentButton" + v-gl-tooltip + data-testid="leftCommentButton" + class="add-diff-note tooltip-wrapper" + :title="addCommentTooltipLeft" > - <gl-icon :size="12" name="comment" /> - </button> - </span> + <button + :draggable="glFeatures.dragCommentSelection" + type="button" + class="add-diff-note note-button js-add-diff-note-button qa-diff-comment" + :class="{ 'gl-cursor-grab': dragging }" + :disabled="line.left.commentsDisabled" + @click="handleCommentButton(line.left)" + @dragstart="onDragStart({ ...line.left, index })" + > + <gl-icon :size="12" name="comment" /> + </button> + </span> + </template> <a - v-if="line.left.old_line" + v-if="line.left.old_line && line.left.type !== $options.CONFLICT_THEIR" :data-linenumber="line.left.old_line" :href="line.lineHrefOld" @click="setHighlightedRow(line.lineCode)" @@ -157,52 +239,87 @@ export default { " /> </div> - <div v-if="inline" :class="classNameMapCellLeft" class="diff-td diff-line-num old_line"> + <div v-if="inline" :class="classNameMapCellLeft" class="diff-td diff-line-num"> <a - v-if="line.left.new_line" + v-if="line.left.new_line && line.left.type !== $options.CONFLICT_OUR" :data-linenumber="line.left.new_line" :href="line.lineHrefOld" @click="setHighlightedRow(line.lineCode)" > </a> </div> - <div :class="parallelViewLeftLineType" class="diff-td line-coverage left-side"></div> + <div + v-gl-tooltip.hover + :title="coverageStateLeft.text" + :class="[...parallelViewLeftLineType, coverageStateLeft.class]" + class="diff-td line-coverage left-side" + ></div> <div :id="line.left.line_code" :key="line.left.line_code" - v-safe-html="line.left.rich_text" - :class="parallelViewLeftLineType" - class="diff-td line_content with-coverage parallel left-side" + :class="[parallelViewLeftLineType, { parallel: !inline }]" + class="diff-td line_content with-coverage left-side" data-testid="leftContent" @mousedown="handleParallelLineMouseDown" - ></div> + > + <strong v-if="isLeftConflictMarker">{{ conflictText(line.left) }}</strong> + <span v-else v-safe-html="line.left.rich_text"></span> + </div> </template> - <template v-else> - <div data-testid="leftEmptyCell" class="diff-td diff-line-num old_line empty-cell"></div> - <div v-if="inline" class="diff-td diff-line-num old_line empty-cell"></div> - <div class="diff-td line-coverage left-side empty-cell"></div> - <div class="diff-td line_content with-coverage parallel left-side empty-cell"></div> + <template v-else-if="!inline || (line.left && line.left.type === $options.CONFLICT_MARKER)"> + <div + data-testid="leftEmptyCell" + class="diff-td diff-line-num old_line empty-cell" + :class="emptyCellLeftClassMap" + > + + </div> + <div + v-if="inline" + class="diff-td diff-line-num old_line empty-cell" + :class="emptyCellLeftClassMap" + ></div> + <div + class="diff-td line-coverage left-side empty-cell" + :class="emptyCellLeftClassMap" + ></div> + <div + class="diff-td line_content with-coverage left-side empty-cell" + :class="[emptyCellLeftClassMap, { parallel: !inline }]" + ></div> </template> </div> - <div v-if="!inline" class="diff-grid-right right-side"> + <div + v-if="!inline" + data-testid="right-side" + class="diff-grid-right right-side" + @dragover.prevent + @dragenter="onDragEnter(line.right, index)" + @dragend="onDragEnd" + > <template v-if="line.right"> <div :class="classNameMapCellRight" class="diff-td diff-line-num new_line"> - <span - v-if="shouldRenderCommentButton" - v-gl-tooltip - data-testid="rightCommentButton" - class="add-diff-note tooltip-wrapper" - :title="addCommentTooltipRight" - > - <button - type="button" - class="add-diff-note note-button js-add-diff-note-button qa-diff-comment" - :disabled="line.right.commentsDisabled" - @click="handleCommentButton(line.right)" + <template v-if="line.right.type !== $options.CONFLICT_MARKER_THEIR"> + <span + v-if="shouldRenderCommentButton" + v-gl-tooltip + data-testid="rightCommentButton" + class="add-diff-note tooltip-wrapper" + :title="addCommentTooltipRight" > - <gl-icon :size="12" name="comment" /> - </button> - </span> + <button + :draggable="glFeatures.dragCommentSelection" + type="button" + class="add-diff-note note-button js-add-diff-note-button qa-diff-comment" + :class="{ 'gl-cursor-grab': dragging }" + :disabled="line.right.commentsDisabled" + @click="handleCommentButton(line.right)" + @dragstart="onDragStart({ ...line.right, index })" + > + <gl-icon :size="12" name="comment" /> + </button> + </span> + </template> <a v-if="line.right.new_line" :data-linenumber="line.right.new_line" @@ -226,8 +343,12 @@ export default { </div> <div v-gl-tooltip.hover - :title="coverageState.text" - :class="[line.right.type, coverageState.class, { hll: isHighlighted }]" + :title="coverageStateRight.text" + :class="[ + line.right.type, + coverageStateRight.class, + { hll: isHighlighted, hll: isCommented }, + ]" class="diff-td line-coverage right-side" ></div> <div @@ -238,17 +359,38 @@ export default { line.right.type, { hll: isHighlighted, + hll: isCommented, + parallel: !inline, }, ]" - class="diff-td line_content with-coverage parallel right-side" + class="diff-td line_content with-coverage right-side" @mousedown="handleParallelLineMouseDown" - ></div> + > + <strong v-if="line.right.type === $options.CONFLICT_MARKER_THEIR">{{ + conflictText(line.right) + }}</strong> + <span v-else v-safe-html="line.right.rich_text"></span> + </div> </template> <template v-else> - <div data-testid="rightEmptyCell" class="diff-td diff-line-num old_line empty-cell"></div> - <div class="diff-td diff-line-num old_line empty-cell"></div> - <div class="diff-td line-coverage right-side empty-cell"></div> - <div class="diff-td line_content with-coverage parallel right-side empty-cell"></div> + <div + data-testid="rightEmptyCell" + class="diff-td diff-line-num old_line empty-cell" + :class="emptyCellRightClassMap" + ></div> + <div + v-if="inline" + class="diff-td diff-line-num old_line empty-cell" + :class="emptyCellRightClassMap" + ></div> + <div + class="diff-td line-coverage right-side empty-cell" + :class="emptyCellRightClassMap" + ></div> + <div + class="diff-td line_content with-coverage right-side empty-cell" + :class="[emptyCellRightClassMap, { parallel: !inline }]" + ></div> </template> </div> </div> diff --git a/app/assets/javascripts/diffs/components/diff_row_utils.js b/app/assets/javascripts/diffs/components/diff_row_utils.js index d5491d3cd56..7606c39ad37 100644 --- a/app/assets/javascripts/diffs/components/diff_row_utils.js +++ b/app/assets/javascripts/diffs/components/diff_row_utils.js @@ -15,27 +15,27 @@ export const isHighlighted = (state, line, isCommented) => { return lineCode ? lineCode === state.diffs.highlightedRow : false; }; -export const isContextLine = type => type === CONTEXT_LINE_TYPE; +export const isContextLine = (type) => type === CONTEXT_LINE_TYPE; -export const isMatchLine = type => type === MATCH_LINE_TYPE; +export const isMatchLine = (type) => type === MATCH_LINE_TYPE; -export const isMetaLine = type => +export const isMetaLine = (type) => [OLD_NO_NEW_LINE_TYPE, NEW_NO_NEW_LINE_TYPE, EMPTY_CELL_TYPE].includes(type); export const shouldRenderCommentButton = (isLoggedIn, isCommentButtonRendered) => { return isCommentButtonRendered && isLoggedIn; }; -export const hasDiscussions = line => line?.discussions?.length > 0; +export const hasDiscussions = (line) => line?.discussions?.length > 0; -export const lineHref = line => `#${line?.line_code || ''}`; +export const lineHref = (line) => `#${line?.line_code || ''}`; -export const lineCode = line => { +export const lineCode = (line) => { if (!line) return undefined; return line.line_code || line.left?.line_code || line.right?.line_code; }; -export const classNameMapCell = (line, hll, isLoggedIn, isHover) => { +export const classNameMapCell = ({ line, hll, isLoggedIn, isHover }) => { if (!line) return []; const { type } = line; @@ -44,15 +44,19 @@ export const classNameMapCell = (line, hll, isLoggedIn, isHover) => { { hll, [LINE_HOVER_CLASS_NAME]: isLoggedIn && isHover && !isContextLine(type) && !isMetaLine(type), + old_line: line.type === 'old', + new_line: line.type === 'new', }, ]; }; -export const addCommentTooltip = line => { +export const addCommentTooltip = (line) => { let tooltip; if (!line) return tooltip; - tooltip = __('Add a comment to this line'); + tooltip = gon.drag_comment_selection + ? __('Add a comment to this line or drag for multiple lines') + : __('Add a comment to this line'); const brokenSymlinks = line.commentsDisabled; if (brokenSymlinks) { @@ -84,7 +88,7 @@ export const shouldShowCommentButton = (hover, context, meta, discussions) => { return hover && !context && !meta && !discussions; }; -export const mapParallel = content => line => { +export const mapParallel = (content) => (line) => { let { left, right } = line; // Dicussions/Comments @@ -137,7 +141,7 @@ export const mapParallel = content => line => { }; // TODO: Delete this function when unifiedDiffComponents FF is removed -export const mapInline = content => line => { +export const mapInline = (content) => (line) => { // Discussions/Comments const renderCommentRow = line.hasForm || (line.discussions?.length && line.discussionsExpanded); diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue index 84429f62a1c..79800f835f4 100644 --- a/app/assets/javascripts/diffs/components/diff_view.vue +++ b/app/assets/javascripts/diffs/components/diff_view.vue @@ -1,5 +1,5 @@ <script> -import { mapGetters, mapState } from 'vuex'; +import { mapGetters, mapState, mapActions } from 'vuex'; import draftCommentsMixin from '~/diffs/mixins/draft_comments'; import DraftNote from '~/batch_comments/components/draft_note.vue'; import DiffRow from './diff_row.vue'; @@ -35,6 +35,12 @@ export default { default: false, }, }, + data() { + return { + dragStart: null, + updatedLineRange: null, + }; + }, computed: { ...mapGetters('diffs', ['commitId']), ...mapState({ @@ -52,12 +58,39 @@ export default { }, }, methods: { + ...mapActions(['setSelectedCommentPosition']), + ...mapActions('diffs', ['showCommentForm']), showCommentLeft(line) { return !this.inline || line.left; }, showCommentRight(line) { return !this.inline || (line.right && !line.left); }, + onStartDragging(line) { + this.dragStart = line; + }, + onDragOver(line) { + if (line.chunk !== this.dragStart.chunk) return; + + let start = this.dragStart; + let end = line; + + if (this.dragStart.index >= line.index) { + start = line; + end = this.dragStart; + } + + this.updatedLineRange = { start, end }; + + this.setSelectedCommentPosition(this.updatedLineRange); + }, + onStopDragging() { + this.showCommentForm({ + lineCode: this.updatedLineRange?.end?.line_code, + fileHash: this.diffFile.file_hash, + }); + this.dragStart = null; + }, }, userColorScheme: window.gon.user_color_scheme, }; @@ -94,6 +127,10 @@ export default { :is-bottom="index + 1 === diffLinesLength" :is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine" :inline="inline" + :index="index" + @enterdragging="onDragOver" + @startdragging="onStartDragging" + @stopdragging="onStopDragging" /> <div v-if="line.renderCommentRow" diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue index 2d8ffb047ca..014b1ebe54b 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue @@ -72,7 +72,12 @@ export default { return this.fileLineCoverage(this.filePath, this.line.new_line); }, classNameMapCell() { - return classNameMapCell(this.line, this.isHighlighted, this.isLoggedIn, this.isHover); + return classNameMapCell({ + line: this.line, + hll: this.isHighlighted, + isLoggedIn: this.isLoggedIn, + isHover: this.isHover, + }); }, addCommentTooltip() { return addCommentTooltip(this.line); diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue index 05f5461054f..28485a2fdac 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_view.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue @@ -58,9 +58,9 @@ export default { class="code diff-wrap-lines js-syntax-highlight text-file js-diff-inline-view" > <colgroup> - <col style="width: 50px;" /> - <col style="width: 50px;" /> - <col style="width: 8px;" /> + <col style="width: 50px" /> + <col style="width: 50px" /> + <col style="width: 8px" /> <col /> </colgroup> <tbody> diff --git a/app/assets/javascripts/diffs/components/no_changes.vue b/app/assets/javascripts/diffs/components/no_changes.vue index a640dcb0a90..e0fdbf6ac3a 100644 --- a/app/assets/javascripts/diffs/components/no_changes.vue +++ b/app/assets/javascripts/diffs/components/no_changes.vue @@ -14,7 +14,31 @@ export default { }, }, computed: { + ...mapGetters('diffs', [ + 'diffCompareDropdownTargetVersions', + 'diffCompareDropdownSourceVersions', + ]), ...mapGetters(['getNoteableData']), + selectedSourceVersion() { + return this.diffCompareDropdownSourceVersions.find((x) => x.selected); + }, + sourceName() { + if (!this.selectedSourceVersion || this.selectedSourceVersion.isLatestVersion) { + return this.getNoteableData.source_branch; + } + + return this.selectedSourceVersion.versionName; + }, + selectedTargetVersion() { + return this.diffCompareDropdownTargetVersions.find((x) => x.selected); + }, + targetName() { + if (!this.selectedTargetVersion || this.selectedTargetVersion.version_index < 0) { + return this.getNoteableData.target_branch; + } + + return this.selectedTargetVersion.versionName || ''; + }, }, }; </script> @@ -26,14 +50,16 @@ export default { </div> <div class="col-12"> <div class="text-content text-center"> - <gl-sprintf :message="__('No changes between %{sourceBranch} and %{targetBranch}')"> - <template #sourceBranch> - <span class="ref-name">{{ getNoteableData.source_branch }}</span> - </template> - <template #targetBranch> - <span class="ref-name">{{ getNoteableData.target_branch }}</span> - </template> - </gl-sprintf> + <div data-testid="no-changes-message"> + <gl-sprintf :message="__('No changes between %{source} and %{target}')"> + <template #source> + <span class="ref-name">{{ sourceName }}</span> + </template> + <template #target> + <span class="ref-name">{{ targetName }}</span> + </template> + </gl-sprintf> + </div> <div class="text-center"> <gl-button :href="getNoteableData.new_blob_path" variant="success" category="primary">{{ __('Create commit') diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue index 13cd0651ff2..47eecef2385 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue @@ -68,20 +68,20 @@ export default { return this.fileLineCoverage(this.filePath, this.line.right.new_line); }, classNameMapCellLeft() { - return utils.classNameMapCell( - this.line.left, - this.isHighlighted, - this.isLoggedIn, - this.isLeftHover, - ); + return utils.classNameMapCell({ + line: this.line.left, + hll: this.isHighlighted, + isLoggedIn: this.isLoggedIn, + isHover: this.isLeftHover, + }); }, classNameMapCellRight() { - return utils.classNameMapCell( - this.line.right, - this.isHighlighted, - this.isLoggedIn, - this.isRightHover, - ); + return utils.classNameMapCell({ + line: this.line.right, + hll: this.isHighlighted, + isLoggedIn: this.isLoggedIn, + isHover: this.isRightHover, + }); }, addCommentTooltipLeft() { return utils.addCommentTooltip(this.line.left); @@ -112,8 +112,8 @@ export default { mounted() { this.scrollToLineIfNeededParallel(this.line); this.unwatchShouldShowCommentButton = this.$watch( - vm => [vm.shouldShowCommentButtonLeft, vm.shouldShowCommentButtonRight].join(), - newVal => { + (vm) => [vm.shouldShowCommentButtonLeft, vm.shouldShowCommentButtonRight].join(), + (newVal) => { if (newVal) { this.isCommentButtonRendered = true; this.unwatchShouldShowCommentButton(); @@ -150,7 +150,7 @@ export default { const table = line.closest('table'); table.removeClass('left-side-selected right-side-selected'); - const [lineClass] = ['left-side', 'right-side'].filter(name => line.hasClass(name)); + const [lineClass] = ['left-side', 'right-side'].filter((name) => line.hasClass(name)); if (lineClass) { table.addClass(`${lineClass}-selected`); diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue index 67b599fe163..21e0bf18dbf 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue @@ -57,11 +57,11 @@ export default { class="code diff-wrap-lines js-syntax-highlight text-file" > <colgroup> - <col style="width: 50px;" /> - <col style="width: 8px;" /> + <col style="width: 50px" /> + <col style="width: 8px" /> <col /> - <col style="width: 50px;" /> - <col style="width: 8px;" /> + <col style="width: 50px" /> + <col style="width: 8px" /> <col /> </colgroup> <tbody> diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index d03d450b12d..1a258695fa0 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -35,7 +35,7 @@ export default { } return this.allBlobs.reduce((acc, folder) => { - const tree = folder.tree.filter(f => f.path.toLowerCase().indexOf(search) >= 0); + const tree = folder.tree.filter((f) => f.path.toLowerCase().indexOf(search) >= 0); if (tree.length) { return acc.concat({ diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index 07e27bd8e47..7080348ee7d 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -109,3 +109,9 @@ export const EVT_PERF_MARK_FILE_TREE_END = 'mr:diffs:perf:fileTreeEnd'; export const EVT_PERF_MARK_DIFF_FILES_START = 'mr:diffs:perf:filesStart'; export const EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN = 'mr:diffs:perf:firstFileShown'; export const EVT_PERF_MARK_DIFF_FILES_END = 'mr:diffs:perf:filesEnd'; + +export const CONFLICT_OUR = 'conflict_our'; +export const CONFLICT_THEIR = 'conflict_their'; +export const CONFLICT_MARKER = 'conflict_marker'; +export const CONFLICT_MARKER_OUR = 'conflict_marker_our'; +export const CONFLICT_MARKER_THEIR = 'conflict_marker_their'; diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js index 587220488be..4e0720c645a 100644 --- a/app/assets/javascripts/diffs/index.js +++ b/app/assets/javascripts/diffs/index.js @@ -5,6 +5,10 @@ import { parseBoolean } from '~/lib/utils/common_utils'; import FindFile from '~/vue_shared/components/file_finder/index.vue'; import eventHub from '../notes/event_hub'; import diffsApp from './components/app.vue'; + +import { getDerivedMergeRequestInformation } from './utils/merge_request'; +import { getReviewsForMergeRequest } from './utils/file_reviews'; + import { TREE_LIST_STORAGE_KEY, DIFF_WHITESPACE_COOKIE_NAME } from './constants'; export default function initDiffsApp(store) { @@ -79,11 +83,12 @@ export default function initDiffsApp(store) { showSuggestPopover: parseBoolean(dataset.showSuggestPopover), showWhitespaceDefault: parseBoolean(dataset.showWhitespaceDefault), viewDiffsFileByFile: parseBoolean(dataset.fileByFileDefault), + defaultSuggestionCommitMessage: dataset.defaultSuggestionCommitMessage, }; }, computed: { ...mapState({ - activeTab: state => state.page.activeTab, + activeTab: (state) => state.page.activeTab, }), }, created() { @@ -102,6 +107,8 @@ export default function initDiffsApp(store) { ...mapActions('diffs', ['setRenderTreeList', 'setShowWhitespace']), }, render(createElement) { + const { mrPath } = getDerivedMergeRequestInformation({ endpoint: this.endpoint }); + return createElement('diffs-app', { props: { endpoint: this.endpoint, @@ -117,6 +124,8 @@ export default function initDiffsApp(store) { dismissEndpoint: this.dismissEndpoint, showSuggestPopover: this.showSuggestPopover, fileByFileUserPreference: this.viewDiffsFileByFile, + defaultSuggestionCommitMessage: this.defaultSuggestionCommitMessage, + mrReviews: getReviewsForMergeRequest(mrPath), }, }); }, diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 5b410051705..e95e9ac3ee4 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -50,6 +50,8 @@ import { } from '../constants'; import { diffViewerModes } from '~/ide/constants'; import { isCollapsed } from '../utils/diff_file'; +import { getDerivedMergeRequestInformation } from '../utils/merge_request'; +import { markFileReview, setReviewsForMergeRequest } from '../utils/file_reviews'; export const setBaseConfig = ({ commit }, options) => { const { @@ -60,7 +62,9 @@ export const setBaseConfig = ({ commit }, options) => { projectPath, dismissEndpoint, showSuggestPopover, + defaultSuggestionCommitMessage, viewDiffsFileByFile, + mrReviews, } = options; commit(types.SET_BASE_CONFIG, { endpoint, @@ -70,7 +74,9 @@ export const setBaseConfig = ({ commit }, options) => { projectPath, dismissEndpoint, showSuggestPopover, + defaultSuggestionCommitMessage, viewDiffsFileByFile, + mrReviews, }); }; @@ -123,7 +129,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => { // We need to check that the currentDiffFileId points to a file that exists if ( state.currentDiffFileId && - !state.diffFiles.some(f => f.file_hash === state.currentDiffFileId) && + !state.diffFiles.some((f) => f.file_hash === state.currentDiffFileId) && !isNoteLink ) { commit(types.VIEW_DIFF_FILE, state.diffFiles[0].file_hash); @@ -131,11 +137,11 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => { if (state.diffFiles?.length) { // eslint-disable-next-line promise/catch-or-return,promise/no-nesting - import('~/code_navigation').then(m => + import('~/code_navigation').then((m) => m.default({ blobs: state.diffFiles - .filter(f => f.code_navigation_path) - .map(f => ({ + .filter((f) => f.code_navigation_path) + .map((f) => ({ path: f.new_path, codeNavigationPath: f.code_navigation_path, })), @@ -157,7 +163,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => { return pagination.next_page; }) - .then(nextPage => nextPage && getBatch(nextPage)) + .then((nextPage) => nextPage && getBatch(nextPage)) .catch(() => commit(types.SET_RETRIEVING_BATCHES, false)); return getBatch() @@ -207,7 +213,7 @@ export const fetchDiffFilesMeta = ({ commit, state }) => { export const fetchCoverageFiles = ({ commit, state }) => { const coveragePoll = new Poll({ resource: { - getCoverageReports: endpoint => axios.get(endpoint), + getCoverageReports: (endpoint) => axios.get(endpoint), }, data: state.endpointCoverage, method: 'getCoverageReports', @@ -242,8 +248,8 @@ export const assignDiscussionsToDiff = ( const hash = getLocationHash(); discussions - .filter(discussion => discussion.diff_discussion) - .forEach(discussion => { + .filter((discussion) => discussion.diff_discussion) + .forEach((discussion) => { commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, { discussion, diffPositionByLineCode, @@ -270,10 +276,10 @@ export const toggleLineDiscussions = ({ commit }, options) => { }; export const renderFileForDiscussionId = ({ commit, rootState, state }, discussionId) => { - const discussion = rootState.notes.discussions.find(d => d.id === discussionId); + const discussion = rootState.notes.discussions.find((d) => d.id === discussionId); if (discussion && discussion.diff_file) { - const file = state.diffFiles.find(f => f.file_hash === discussion.diff_file.file_hash); + const file = state.diffFiles.find((f) => f.file_hash === discussion.diff_file.file_hash); if (file) { if (!file.renderIt) { @@ -299,11 +305,12 @@ export const renderFileForDiscussionId = ({ commit, rootState, state }, discussi export const startRenderDiffsQueue = ({ state, commit }) => { const checkItem = () => - new Promise(resolve => { + new Promise((resolve) => { const nextFile = state.diffFiles.find( - file => + (file) => !file.renderIt && - (file.viewer && (!isCollapsed(file) || file.viewer.name !== diffViewerModes.text)), + file.viewer && + (!isCollapsed(file) || file.viewer.name !== diffViewerModes.text), ); if (nextFile) { @@ -357,7 +364,7 @@ export const loadMoreLines = ({ commit }, options) => { params.from_merge_request = true; - return axios.get(endpoint, { params }).then(res => { + return axios.get(endpoint, { params }).then((res) => { const contextLines = res.data || []; commit(types.ADD_CONTEXT_LINES, { @@ -398,7 +405,7 @@ export const loadCollapsedDiff = ({ commit, getters, state }, file) => w: state.showWhitespace ? '0' : '1', }, }) - .then(res => { + .then((res) => { commit(types.ADD_COLLAPSED_DIFFS, { file, data: res.data, @@ -421,7 +428,7 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => { const shouldCloseAll = getters.diffHasAllExpandedDiscussions(diff); const shouldExpandAll = getters.diffHasAllCollapsedDiscussions(diff); - discussions.forEach(discussion => { + discussions.forEach((discussion) => { const data = { discussionId: discussion.id }; if (shouldCloseAll) { @@ -435,13 +442,13 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => { export const toggleFileDiscussionWrappers = ({ commit }, diff) => { const discussionWrappersExpanded = allDiscussionWrappersExpanded(diff); const lineCodesWithDiscussions = new Set(); - const lineHasDiscussion = line => Boolean(line?.discussions.length); - const registerDiscussionLine = line => lineCodesWithDiscussions.add(line.line_code); + const lineHasDiscussion = (line) => Boolean(line?.discussions.length); + const registerDiscussionLine = (line) => lineCodesWithDiscussions.add(line.line_code); diff[INLINE_DIFF_LINES_KEY].filter(lineHasDiscussion).forEach(registerDiscussionLine); if (lineCodesWithDiscussions.size) { - Array.from(lineCodesWithDiscussions).forEach(lineCode => { + Array.from(lineCodesWithDiscussions).forEach((lineCode) => { commit(types.TOGGLE_LINE_DISCUSSIONS, { fileHash: diff.file_hash, expanded: !discussionWrappersExpanded, @@ -459,8 +466,8 @@ export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => { }); return dispatch('saveNote', postData, { root: true }) - .then(result => dispatch('updateDiscussion', result.discussion, { root: true })) - .then(discussion => dispatch('assignDiscussionsToDiff', [discussion])) + .then((result) => dispatch('updateDiscussion', result.discussion, { root: true })) + .then((discussion) => dispatch('assignDiscussionsToDiff', [discussion])) .then(() => dispatch('updateResolvableDiscussionsCounts', null, { root: true })) .then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash)) .catch(() => createFlash(s__('MergeRequests|Saving the comment failed'))); @@ -560,7 +567,7 @@ export const setExpandedDiffLines = ({ commit }, { file, data }) => { }); commit(types.TOGGLE_DIFF_FILE_RENDERING_MORE, file.file_path); - const idleCb = t => { + const idleCb = (t) => { const startIndex = index; while ( @@ -608,7 +615,7 @@ export const fetchFullDiff = ({ commit, dispatch }, file) => .catch(() => dispatch('receiveFullDiffError', file.file_path)); export const toggleFullDiff = ({ dispatch, commit, getters, state }, filePath) => { - const file = state.diffFiles.find(f => f.file_path === filePath); + const file = state.diffFiles.find((f) => f.file_path === filePath); commit(types.REQUEST_FULL_DIFF, filePath); @@ -719,7 +726,7 @@ export const setCurrentDiffFileIdFromNote = ({ commit, state, rootGetters }, not const fileHash = rootGetters.getDiscussion(note.discussion_id).diff_file?.file_hash; - if (fileHash && state.diffFiles.some(f => f.file_hash === fileHash)) { + if (fileHash && state.diffFiles.some((f) => f.file_hash === fileHash)) { commit(types.VIEW_DIFF_FILE, fileHash); } }; @@ -741,3 +748,13 @@ export const setFileByFile = ({ commit }, { fileByFile }) => { mergeUrlParams({ [DIFF_FILE_BY_FILE_COOKIE_NAME]: fileViewMode }, window.location.href), ); }; + +export function reviewFile({ commit, state, getters }, { file, reviewed = true }) { + const { mrPath } = getDerivedMergeRequestInformation({ endpoint: file.load_collapsed_diff_url }); + const reviews = setReviewsForMergeRequest( + mrPath, + markFileReview(getters.fileReviews(state), file, reviewed), + ); + + commit(types.SET_MR_FILE_REVIEWS, reviews); +} diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js index baf54188932..a167b6d4694 100644 --- a/app/assets/javascripts/diffs/store/getters.js +++ b/app/assets/javascripts/diffs/store/getters.js @@ -1,5 +1,6 @@ import { __, n__ } from '~/locale'; import { parallelizeDiffLines } from './utils'; +import { isFileReviewed } from '../utils/file_reviews'; import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE, @@ -8,13 +9,13 @@ import { export * from './getters_versions_dropdowns'; -export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE; +export const isParallelView = (state) => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE; -export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE; +export const isInlineView = (state) => state.diffViewType === INLINE_DIFF_VIEW_TYPE; -export const whichCollapsedTypes = state => { - const automatic = state.diffFiles.some(file => file.viewer?.automaticallyCollapsed); - const manual = state.diffFiles.some(file => file.viewer?.manuallyCollapsed); +export const whichCollapsedTypes = (state) => { + const automatic = state.diffFiles.some((file) => file.viewer?.automaticallyCollapsed); + const manual = state.diffFiles.some((file) => file.viewer?.manuallyCollapsed); return { any: automatic || manual, @@ -23,18 +24,18 @@ export const whichCollapsedTypes = state => { }; }; -export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null); +export const commitId = (state) => (state.commit && state.commit.id ? state.commit.id : null); /** * Checks if the diff has all discussions expanded * @param {Object} diff * @returns {Boolean} */ -export const diffHasAllExpandedDiscussions = (state, getters) => diff => { +export const diffHasAllExpandedDiscussions = (state, getters) => (diff) => { const discussions = getters.getDiffFileDiscussions(diff); return ( - (discussions && discussions.length && discussions.every(discussion => discussion.expanded)) || + (discussions && discussions.length && discussions.every((discussion) => discussion.expanded)) || false ); }; @@ -44,11 +45,13 @@ export const diffHasAllExpandedDiscussions = (state, getters) => diff => { * @param {Object} diff * @returns {Boolean} */ -export const diffHasAllCollapsedDiscussions = (state, getters) => diff => { +export const diffHasAllCollapsedDiscussions = (state, getters) => (diff) => { const discussions = getters.getDiffFileDiscussions(diff); return ( - (discussions && discussions.length && discussions.every(discussion => !discussion.expanded)) || + (discussions && + discussions.length && + discussions.every((discussion) => !discussion.expanded)) || false ); }; @@ -58,9 +61,9 @@ export const diffHasAllCollapsedDiscussions = (state, getters) => diff => { * @param {Object} diff * @returns {Boolean} */ -export const diffHasExpandedDiscussions = () => diff => { - return diff[INLINE_DIFF_LINES_KEY].filter(l => l.discussions.length >= 1).some( - l => l.discussionsExpanded, +export const diffHasExpandedDiscussions = () => (diff) => { + return diff[INLINE_DIFF_LINES_KEY].filter((l) => l.discussions.length >= 1).some( + (l) => l.discussionsExpanded, ); }; @@ -69,8 +72,8 @@ export const diffHasExpandedDiscussions = () => diff => { * @param {Boolean} diff * @returns {Boolean} */ -export const diffHasDiscussions = () => diff => { - return diff[INLINE_DIFF_LINES_KEY].some(l => l.discussions.length >= 1); +export const diffHasDiscussions = () => (diff) => { + return diff[INLINE_DIFF_LINES_KEY].some((l) => l.discussions.length >= 1); }; /** @@ -78,22 +81,22 @@ export const diffHasDiscussions = () => diff => { * @param {Object} diff * @returns {Array} */ -export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) => diff => +export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) => (diff) => rootGetters.discussions.filter( - discussion => discussion.diff_discussion && discussion.diff_file.file_hash === diff.file_hash, + (discussion) => discussion.diff_discussion && discussion.diff_file.file_hash === diff.file_hash, ) || []; -export const getDiffFileByHash = state => fileHash => - state.diffFiles.find(file => file.file_hash === fileHash); +export const getDiffFileByHash = (state) => (fileHash) => + state.diffFiles.find((file) => file.file_hash === fileHash); -export const flatBlobsList = state => - Object.values(state.treeEntries).filter(f => f.type === 'blob'); +export const flatBlobsList = (state) => + Object.values(state.treeEntries).filter((f) => f.type === 'blob'); export const allBlobs = (state, getters) => getters.flatBlobsList.reduce((acc, file) => { const { parentPath } = file; - if (parentPath && !acc.some(f => f.path === parentPath)) { + if (parentPath && !acc.some((f) => f.path === parentPath)) { acc.push({ path: parentPath, isHeader: true, @@ -101,13 +104,13 @@ export const allBlobs = (state, getters) => }); } - acc.find(f => f.path === parentPath).tree.push(file); + acc.find((f) => f.path === parentPath).tree.push(file); return acc; }, []); -export const getCommentFormForDiffFile = state => fileHash => - state.commentForms.find(form => form.fileHash === fileHash); +export const getCommentFormForDiffFile = (state) => (fileHash) => + state.commentForms.find((form) => form.fileHash === fileHash); /** * Returns the test coverage hits for a specific line of a given file @@ -115,7 +118,7 @@ export const getCommentFormForDiffFile = state => fileHash => * @param {number} line * @returns {number} */ -export const fileLineCoverage = state => (file, line) => { +export const fileLineCoverage = (state) => (file, line) => { if (!state.coverageFiles.files) return {}; const fileCoverage = state.coverageFiles.files[file]; if (!fileCoverage) return {}; @@ -136,10 +139,13 @@ export const fileLineCoverage = state => (file, line) => { * Returns index of a currently selected diff in diffFiles * @returns {number} */ -export const currentDiffIndex = state => - Math.max(0, state.diffFiles.findIndex(diff => diff.file_hash === state.currentDiffFileId)); +export const currentDiffIndex = (state) => + Math.max( + 0, + state.diffFiles.findIndex((diff) => diff.file_hash === state.currentDiffFileId), + ); -export const diffLines = state => (file, unifiedDiffComponents) => { +export const diffLines = (state) => (file, unifiedDiffComponents) => { if (!unifiedDiffComponents && state.diffViewType === INLINE_DIFF_VIEW_TYPE) { return null; } @@ -149,3 +155,7 @@ export const diffLines = state => (file, unifiedDiffComponents) => { state.diffViewType === INLINE_DIFF_VIEW_TYPE, ); }; + +export function fileReviews(state) { + return state.diffFiles.map((file) => isFileReviewed(state.mrReviews, file)); +} diff --git a/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js index 135b1c61ef5..3f33b0c900e 100644 --- a/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js +++ b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js @@ -2,10 +2,10 @@ import { __, n__, sprintf } from '~/locale'; import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils'; import { DIFF_COMPARE_BASE_VERSION_INDEX, DIFF_COMPARE_HEAD_VERSION_INDEX } from '../constants'; -export const selectedTargetIndex = state => +export const selectedTargetIndex = (state) => state.startVersion?.version_index || DIFF_COMPARE_BASE_VERSION_INDEX; -export const selectedSourceIndex = state => state.mergeRequestDiff.version_index; +export const selectedSourceIndex = (state) => state.mergeRequestDiff.version_index; export const diffCompareDropdownTargetVersions = (state, getters) => { // startVersion only exists if the user has selected a version other @@ -40,7 +40,7 @@ export const diffCompareDropdownTargetVersions = (state, getters) => { selected: isHeadSelected, }; // Appended properties here are to make the compare_dropdown_layout easier to reason about - const formatVersion = v => { + const formatVersion = (v) => { return { href: v.compare_path, versionName: sprintf(__(`version %{versionIndex}`), { versionIndex: v.version_index }), @@ -53,19 +53,23 @@ export const diffCompareDropdownTargetVersions = (state, getters) => { ...state.mergeRequestDiffs.slice(1).map(formatVersion), baseVersion, state.mergeRequestDiff.head_version_path && headVersion, - ].filter(a => a); + ].filter((a) => a); }; export const diffCompareDropdownSourceVersions = (state, getters) => { // Appended properties here are to make the compare_dropdown_layout easier to reason about - return state.mergeRequestDiffs.map((v, i) => ({ - ...v, - href: v.version_path, - commitsText: n__(`%d commit,`, `%d commits,`, v.commits_count), - versionName: - i === 0 + return state.mergeRequestDiffs.map((v, i) => { + const isLatestVersion = i === 0; + + return { + ...v, + href: v.version_path, + commitsText: n__(`%d commit,`, `%d commits,`, v.commits_count), + isLatestVersion, + versionName: isLatestVersion ? __('latest version') : sprintf(__(`version %{versionIndex}`), { versionIndex: v.version_index }), - selected: v.version_index === getters.selectedSourceIndex, - })); + selected: v.version_index === getters.selectedSourceIndex, + }; + }); }; diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index c331e52c887..aa89c74cef0 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -45,4 +45,6 @@ export default () => ({ fileFinderVisible: false, dismissEndpoint: '', showSuggestPopover: true, + defaultSuggestionCommitMessage: '', + mrReviews: {}, }); diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index 30097239aaa..4641731c4b6 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -7,6 +7,8 @@ export const SET_DIFF_METADATA = 'SET_DIFF_METADATA'; export const SET_DIFF_DATA_BATCH = 'SET_DIFF_DATA_BATCH'; export const SET_DIFF_FILES = 'SET_DIFF_FILES'; +export const SET_MR_FILE_REVIEWS = 'SET_MR_FILE_REVIEWS'; + export const SET_DIFF_VIEW_TYPE = 'SET_DIFF_VIEW_TYPE'; export const SET_COVERAGE_DATA = 'SET_COVERAGE_DATA'; export const SET_MERGE_REQUEST_DIFFS = 'SET_MERGE_REQUEST_DIFFS'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 19122c3096f..06f0f2c3dfb 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -36,7 +36,9 @@ export default { projectPath, dismissEndpoint, showSuggestPopover, + defaultSuggestionCommitMessage, viewDiffsFileByFile, + mrReviews, } = options; Object.assign(state, { endpoint, @@ -46,7 +48,9 @@ export default { projectPath, dismissEndpoint, showSuggestPopover, + defaultSuggestionCommitMessage, viewDiffsFileByFile, + mrReviews, }); }, @@ -103,11 +107,11 @@ export default { }, [types.TOGGLE_LINE_HAS_FORM](state, { lineCode, fileHash, hasForm }) { - const diffFile = state.diffFiles.find(f => f.file_hash === fileHash); + const diffFile = state.diffFiles.find((f) => f.file_hash === fileHash); if (!diffFile) return; - diffFile[INLINE_DIFF_LINES_KEY].find(l => l.line_code === lineCode).hasForm = hasForm; + diffFile[INLINE_DIFF_LINES_KEY].find((l) => l.line_code === lineCode).hasForm = hasForm; }, [types.ADD_CONTEXT_LINES](state, options) { @@ -123,7 +127,7 @@ export default { bottom, isExpandDown, nextLineNumbers, - ).map(line => { + ).map((line) => { const lineCode = line.type === 'match' ? `${fileHash}_${line.meta_data.old_pos}_${line.meta_data.new_pos}_match` @@ -147,8 +151,8 @@ export default { [types.ADD_COLLAPSED_DIFFS](state, { file, data }) { const files = prepareDiffData({ diff: data }); - const [newFileData] = files.filter(f => f.file_hash === file.file_hash); - const selectedFile = state.diffFiles.find(f => f.file_hash === file.file_hash); + const [newFileData] = files.filter((f) => f.file_hash === file.file_hash); + const selectedFile = state.diffFiles.find((f) => f.file_hash === file.file_hash); Object.assign(selectedFile, { ...newFileData }); }, @@ -157,9 +161,9 @@ export default { const discussionLineCodes = [discussion.line_code, ...(discussion.line_codes || [])]; const fileHash = discussion.diff_file.file_hash; - const lineCheck = line => + const lineCheck = (line) => discussionLineCodes.some( - discussionLineCode => + (discussionLineCode) => line.line_code === discussionLineCode && isDiscussionApplicableToLine({ discussion, @@ -177,26 +181,26 @@ export default { : [], }); - const setDiscussionsExpanded = line => { + const setDiscussionsExpanded = (line) => { const isLineNoteTargeted = line.discussions && line.discussions.some( - disc => disc.notes && disc.notes.find(note => hash === `note_${note.id}`), + (disc) => disc.notes && disc.notes.find((note) => hash === `note_${note.id}`), ); return { ...line, discussionsExpanded: line.discussions && line.discussions.length - ? line.discussions.some(disc => !disc.resolved) || isLineNoteTargeted + ? line.discussions.some((disc) => !disc.resolved) || isLineNoteTargeted : false, }; }; - state.diffFiles.forEach(file => { + state.diffFiles.forEach((file) => { if (file.file_hash === fileHash) { if (file[INLINE_DIFF_LINES_KEY].length) { - file[INLINE_DIFF_LINES_KEY].forEach(line => { + file[INLINE_DIFF_LINES_KEY].forEach((line) => { Object.assign( line, setDiscussionsExpanded(lineCheck(line) ? mapDiscussions(line) : line), @@ -206,7 +210,7 @@ export default { if (!file[INLINE_DIFF_LINES_KEY].length) { const newDiscussions = (file.discussions || []) - .filter(d => d.id !== discussion.id) + .filter((d) => d.id !== discussion.id) .concat(discussion); Object.assign(file, { @@ -218,26 +222,26 @@ export default { }, [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) { - const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash); + const selectedFile = state.diffFiles.find((f) => f.file_hash === fileHash); if (selectedFile) { - updateLineInFile(selectedFile, lineCode, line => + updateLineInFile(selectedFile, lineCode, (line) => Object.assign(line, { - discussions: line.discussions.filter(discussion => discussion.notes.length), + discussions: line.discussions.filter((discussion) => discussion.notes.length), }), ); if (selectedFile.discussions && selectedFile.discussions.length) { selectedFile.discussions = selectedFile.discussions.filter( - discussion => discussion.notes.length, + (discussion) => discussion.notes.length, ); } } }, [types.TOGGLE_LINE_DISCUSSIONS](state, { fileHash, lineCode, expanded }) { - const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash); + const selectedFile = state.diffFiles.find((f) => f.file_hash === fileHash); - updateLineInFile(selectedFile, lineCode, line => { + updateLineInFile(selectedFile, lineCode, (line) => { Object.assign(line, { discussionsExpanded: expanded }); }); }, @@ -260,7 +264,7 @@ export default { [types.UPDATE_DIFF_FILE_COMMENT_FORM](state, formData) { const { fileHash } = formData; - state.commentForms = state.commentForms.map(form => { + state.commentForms = state.commentForms.map((form) => { if (form.fileHash === fileHash) { return { ...formData, @@ -271,7 +275,7 @@ export default { }); }, [types.CLOSE_DIFF_FILE_COMMENT_FORM](state, fileHash) { - state.commentForms = state.commentForms.filter(form => form.fileHash !== fileHash); + state.commentForms = state.commentForms.filter((form) => form.fileHash !== fileHash); }, [types.SET_HIGHLIGHTED_ROW](state, lineCode) { state.highlightedRow = lineCode; @@ -311,7 +315,7 @@ export default { state, { filePath, collapsed, trigger = DIFF_FILE_AUTOMATIC_COLLAPSE }, ) { - const file = state.diffFiles.find(f => f.file_path === filePath); + const file = state.diffFiles.find((f) => f.file_path === filePath); if (file && file.viewer) { if (trigger === DIFF_FILE_MANUAL_COLLAPSE) { @@ -328,17 +332,17 @@ export default { } }, [types.SET_CURRENT_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) { - const file = state.diffFiles.find(f => f.file_path === filePath); + const file = state.diffFiles.find((f) => f.file_path === filePath); file[INLINE_DIFF_LINES_KEY] = lines; }, [types.ADD_CURRENT_VIEW_DIFF_FILE_LINES](state, { filePath, line }) { - const file = state.diffFiles.find(f => f.file_path === filePath); + const file = state.diffFiles.find((f) => f.file_path === filePath); file[INLINE_DIFF_LINES_KEY].push(line); }, [types.TOGGLE_DIFF_FILE_RENDERING_MORE](state, filePath) { - const file = state.diffFiles.find(f => f.file_path === filePath); + const file = state.diffFiles.find((f) => f.file_path === filePath); file.renderingLines = !file.renderingLines; }, @@ -353,4 +357,7 @@ export default { [types.SET_FILE_BY_FILE](state, fileByFile) { state.viewDiffsFileByFile = fileByFile; }, + [types.SET_MR_FILE_REVIEWS](state, newReviews) { + state.mrReviews = newReviews; + }, }; diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index 1839df12c96..c52da558be2 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -15,13 +15,23 @@ import { INLINE_DIFF_LINES_KEY, SHOW_WHITESPACE, NO_SHOW_WHITESPACE, + CONFLICT_OUR, + CONFLICT_THEIR, + CONFLICT_MARKER, + CONFLICT_MARKER_OUR, + CONFLICT_MARKER_THEIR, } from '../constants'; import { prepareRawDiffFile } from '../utils/diff_file'; -export const isAdded = line => ['new', 'new-nonewline'].includes(line.type); -export const isRemoved = line => ['old', 'old-nonewline'].includes(line.type); -export const isUnchanged = line => !line.type; -export const isMeta = line => ['match', 'new-nonewline', 'old-nonewline'].includes(line.type); +export const isAdded = (line) => ['new', 'new-nonewline'].includes(line.type); +export const isRemoved = (line) => ['old', 'old-nonewline'].includes(line.type); +export const isUnchanged = (line) => !line.type; +export const isMeta = (line) => ['match', 'new-nonewline', 'old-nonewline'].includes(line.type); +export const isConflictMarker = (line) => + [CONFLICT_MARKER_OUR, CONFLICT_MARKER_THEIR].includes(line.type); +export const isConflictSeperator = (line) => line.type === CONFLICT_MARKER; +export const isConflictOur = (line) => line.type === CONFLICT_OUR; +export const isConflictTheir = (line) => line.type === CONFLICT_THEIR; /** * Pass in the inline diff lines array which gets converted @@ -42,12 +52,22 @@ export const isMeta = line => ['match', 'new-nonewline', 'old-nonewline'].includ export const parallelizeDiffLines = (diffLines, inline) => { let freeRightIndex = null; + let conflictStartIndex = -1; const lines = []; + // `chunk` is used for dragging to select diff lines + // we are restricting commenting to only lines that appear between + // "expansion rows". Here equal chunks are lines grouped together + // inbetween expansion rows. + let chunk = 0; + for (let i = 0, diffLinesLength = diffLines.length, index = 0; i < diffLinesLength; i += 1) { const line = diffLines[i]; + line.chunk = chunk; + + if (isMeta(line)) chunk += 1; - if (isRemoved(line) || inline) { + if (isRemoved(line) || isConflictOur(line) || inline) { lines.push({ [LINE_POSITION_LEFT]: line, [LINE_POSITION_RIGHT]: null, @@ -58,7 +78,7 @@ export const parallelizeDiffLines = (diffLines, inline) => { freeRightIndex = index; } index += 1; - } else if (isAdded(line)) { + } else if (isAdded(line) || isConflictTheir(line)) { if (freeRightIndex !== null) { // If an old line came before this without a line on the right, this // line can be put to the right of it. @@ -77,15 +97,28 @@ export const parallelizeDiffLines = (diffLines, inline) => { freeRightIndex = null; index += 1; } - } else if (isMeta(line) || isUnchanged(line)) { - // line in the right panel is the same as in the left one - lines.push({ - [LINE_POSITION_LEFT]: line, - [LINE_POSITION_RIGHT]: line, - }); + } else if ( + isMeta(line) || + isUnchanged(line) || + isConflictMarker(line) || + (isConflictSeperator(line) && inline) + ) { + if (conflictStartIndex <= 0) { + // line in the right panel is the same as in the left one + lines.push({ + [LINE_POSITION_LEFT]: line, + [LINE_POSITION_RIGHT]: !inline && line, + }); - freeRightIndex = null; - index += 1; + if (!inline && isConflictMarker(line)) { + conflictStartIndex = index; + } + freeRightIndex = null; + index += 1; + } else { + lines[conflictStartIndex][LINE_POSITION_RIGHT] = line; + conflictStartIndex = -1; + } } } @@ -93,10 +126,10 @@ export const parallelizeDiffLines = (diffLines, inline) => { }; export function findDiffFile(files, match, matchKey = 'file_hash') { - return files.find(file => file[matchKey] === match); + return files.find((file) => file[matchKey] === match); } -export const getReversePosition = linePosition => { +export const getReversePosition = (linePosition) => { if (linePosition === LINE_POSITION_RIGHT) { return LINE_POSITION_LEFT; } @@ -173,7 +206,7 @@ export const findIndexInInlineLines = (lines, lineNumbers) => { const { oldLineNumber, newLineNumber } = lineNumbers; return lines.findIndex( - line => line.old_line === oldLineNumber && line.new_line === newLineNumber, + (line) => line.old_line === oldLineNumber && line.new_line === newLineNumber, ); }; @@ -346,7 +379,7 @@ export function prepareLineForRenamedFile({ line, diffFile, index = 0 }) { function prepareDiffFileLines(file) { const inlineLines = file[INLINE_DIFF_LINES_KEY]; - inlineLines.forEach(line => prepareLine(line, file)); // WARNING: In-Place Mutations! + inlineLines.forEach((line) => prepareLine(line, file)); // WARNING: In-Place Mutations! Object.assign(file, { inlineLinesCount: inlineLines.length, @@ -400,7 +433,7 @@ export function getDiffPositionByLineCode(diffFiles) { let lines = []; lines = diffFiles.reduce((acc, diffFile) => { - diffFile[INLINE_DIFF_LINES_KEY].forEach(line => { + diffFile[INLINE_DIFF_LINES_KEY].forEach((line) => { acc.push({ file: diffFile, line }); }); @@ -447,21 +480,21 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD ...(discussion.positions || []), ]; - const removeLineRange = position => { + const removeLineRange = (position) => { const { line_range: pNotUsed, ...positionNoLineRange } = position; return positionNoLineRange; }; return discussionPositions .map(removeLineRange) - .some(position => isEqual(position, diffPositionCopy)); + .some((position) => isEqual(position, diffPositionCopy)); } // eslint-disable-next-line return latestDiff && discussion.active && line_code === discussion.line_code; } -export const getLowestSingleFolder = folder => { +export const getLowestSingleFolder = (folder) => { const getFolder = (blob, start = []) => blob.tree.reduce( (acc, file) => { @@ -493,8 +526,8 @@ export const getLowestSingleFolder = folder => { }; }; -export const flattenTree = tree => { - const flatten = blobTree => +export const flattenTree = (tree) => { + const flatten = (blobTree) => blobTree.reduce((acc, file) => { const blob = file; let treeToFlatten = blob.tree; @@ -516,7 +549,7 @@ export const flattenTree = tree => { return flatten(tree); }; -export const generateTreeList = files => { +export const generateTreeList = (files) => { const { treeEntries, tree } = files.reduce( (acc, file) => { const split = file.new_path.split('/'); @@ -566,8 +599,8 @@ export const generateTreeList = files => { return { treeEntries, tree: flattenTree(tree) }; }; -export const getDiffMode = diffFile => { - const diffModeKey = Object.keys(diffModes).find(key => diffFile[`${key}_file`]); +export const getDiffMode = (diffFile) => { + const diffModeKey = Object.keys(diffModes).find((key) => diffFile[`${key}_file`]); return ( diffModes[diffModeKey] || (diffFile.viewer && @@ -615,11 +648,11 @@ export const convertExpandLines = ({ return lines; }; -export const idleCallback = cb => requestIdleCallback(cb); +export const idleCallback = (cb) => requestIdleCallback(cb); function getLinesFromFileByLineCode(file, lineCode) { const inlineLines = file[INLINE_DIFF_LINES_KEY]; - const matchesCode = line => line.line_code === lineCode; + const matchesCode = (line) => line.line_code === lineCode; return inlineLines.filter(matchesCode); } @@ -628,15 +661,15 @@ export const updateLineInFile = (selectedFile, lineCode, updateFn) => { getLinesFromFileByLineCode(selectedFile, lineCode).forEach(updateFn); }; -export const allDiscussionWrappersExpanded = diff => { +export const allDiscussionWrappersExpanded = (diff) => { let discussionsExpanded = true; - const changeExpandedResult = line => { + const changeExpandedResult = (line) => { if (line && line.discussions.length) { discussionsExpanded = discussionsExpanded && line.discussionsExpanded; } }; - diff[INLINE_DIFF_LINES_KEY].forEach(line => { + diff[INLINE_DIFF_LINES_KEY].forEach((line) => { changeExpandedResult(line); }); diff --git a/app/assets/javascripts/diffs/utils/diff_file.js b/app/assets/javascripts/diffs/utils/diff_file.js index 69d0e49e501..ce0398e75fc 100644 --- a/app/assets/javascripts/diffs/utils/diff_file.js +++ b/app/assets/javascripts/diffs/utils/diff_file.js @@ -4,11 +4,12 @@ import { DIFF_FILE_MANUAL_COLLAPSE, DIFF_FILE_AUTOMATIC_COLLAPSE, } from '../constants'; +import { getDerivedMergeRequestInformation } from './merge_request'; import { uuids } from './uuids'; function fileSymlinkInformation(file, fileList) { - const duplicates = fileList.filter(iteratedFile => iteratedFile.file_hash === file.file_hash); - const includesSymlink = duplicates.some(iteratedFile => { + const duplicates = fileList.filter((iteratedFile) => iteratedFile.file_hash === file.file_hash); + const includesSymlink = duplicates.some((iteratedFile) => { return [iteratedFile.a_mode, iteratedFile.b_mode].includes(DIFF_FILE_SYMLINK_MODE); }); const brokenSymlinkScenario = duplicates.length > 1 && includesSymlink; @@ -34,8 +35,12 @@ function collapsed(file) { } function identifier(file) { + const { userOrGroup, project, id } = getDerivedMergeRequestInformation({ + endpoint: file.load_collapsed_diff_url, + }); + return uuids({ - seeds: [file.file_identifier_hash, file.blob?.id], + seeds: [userOrGroup, project, id, file.file_identifier_hash, file.blob?.id], })[0]; } @@ -48,10 +53,10 @@ export function prepareRawDiffFile({ file, allFiles, meta = false }) { }, }; - // It's possible, but not confirmed, that `content_sha` isn't available sometimes + // It's possible, but not confirmed, that `blob.id` isn't available sometimes // See: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49506#note_464692057 // We don't want duplicate IDs if that's the case, so we just don't assign an ID - if (!meta && file.blob?.id) { + if (!meta && file.blob?.id && file.load_collapsed_diff_url) { additionalProperties.id = identifier(file); } diff --git a/app/assets/javascripts/diffs/utils/file_reviews.js b/app/assets/javascripts/diffs/utils/file_reviews.js new file mode 100644 index 00000000000..0047955643a --- /dev/null +++ b/app/assets/javascripts/diffs/utils/file_reviews.js @@ -0,0 +1,61 @@ +function getFileReviewsKey(mrPath) { + return `${mrPath}-file-reviews`; +} + +export function getReviewsForMergeRequest(mrPath) { + const reviewsForMr = localStorage.getItem(getFileReviewsKey(mrPath)); + let reviews = {}; + + if (reviewsForMr) { + try { + reviews = JSON.parse(reviewsForMr); + } catch (err) { + reviews = {}; + } + } + + return reviews; +} + +export function setReviewsForMergeRequest(mrPath, reviews) { + localStorage.setItem(getFileReviewsKey(mrPath), JSON.stringify(reviews)); + + return reviews; +} + +export function isFileReviewed(reviews, file) { + const fileReviews = reviews[file.file_identifier_hash]; + + return file?.id && fileReviews?.length ? new Set(fileReviews).has(file.id) : false; +} + +export function reviewable(file) { + return Boolean(file.id) && Boolean(file.file_identifier_hash); +} + +export function markFileReview(reviews, file, reviewed = true) { + const usableReviews = { ...(reviews || {}) }; + let updatedReviews = usableReviews; + let fileReviews; + + if (reviewable(file)) { + fileReviews = new Set([...(usableReviews[file.file_identifier_hash] || [])]); + + if (reviewed) { + fileReviews.add(file.id); + } else { + fileReviews.delete(file.id); + } + + updatedReviews = { + ...usableReviews, + [file.file_identifier_hash]: Array.from(fileReviews), + }; + + if (updatedReviews[file.file_identifier_hash].length === 0) { + delete updatedReviews[file.file_identifier_hash]; + } + } + + return updatedReviews; +} diff --git a/app/assets/javascripts/diffs/utils/merge_request.js b/app/assets/javascripts/diffs/utils/merge_request.js new file mode 100644 index 00000000000..edb4304f558 --- /dev/null +++ b/app/assets/javascripts/diffs/utils/merge_request.js @@ -0,0 +1,20 @@ +const endpointRE = /^(\/?(.+?)\/(.+?)\/-\/merge_requests\/(\d+)).*$/i; + +export function getDerivedMergeRequestInformation({ endpoint } = {}) { + let mrPath; + let userOrGroup; + let project; + let id; + const matches = endpointRE.exec(endpoint); + + if (matches) { + [, mrPath, userOrGroup, project, id] = matches; + } + + return { + mrPath, + userOrGroup, + project, + id, + }; +} diff --git a/app/assets/javascripts/diffs/utils/uuids.js b/app/assets/javascripts/diffs/utils/uuids.js index 12448350e62..1fe5f9f6499 100644 --- a/app/assets/javascripts/diffs/utils/uuids.js +++ b/app/assets/javascripts/diffs/utils/uuids.js @@ -11,7 +11,7 @@ * @typedef {String} UUIDv4 */ -import MersenneTwister from 'mersenne-twister'; +import { MersenneTwister } from 'fast-mersenne-twister'; import stringHash from 'string-hash'; import { isString } from 'lodash'; import { v4 } from 'uuid'; @@ -49,7 +49,7 @@ function randomValuesForUuid(prng) { const buffer = new ArrayBuffer(4); const view = new DataView(buffer); - view.setUint32(0, prng.random_int()); + view.setUint32(0, prng.randomNumber()); randomValues.push(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3)); } diff --git a/app/assets/javascripts/diffs/workers/tree_worker.js b/app/assets/javascripts/diffs/workers/tree_worker.js index 415c463fd19..2fa1934439e 100644 --- a/app/assets/javascripts/diffs/workers/tree_worker.js +++ b/app/assets/javascripts/diffs/workers/tree_worker.js @@ -2,7 +2,7 @@ import { sortTree } from '~/ide/stores/utils'; import { generateTreeList } from '../store/utils'; // eslint-disable-next-line no-restricted-globals -self.addEventListener('message', e => { +self.addEventListener('message', (e) => { const { data } = e; if (data === undefined) { diff --git a/app/assets/javascripts/dirty_submit/dirty_submit_collection.js b/app/assets/javascripts/dirty_submit/dirty_submit_collection.js index 42b051b2270..4886125d4ac 100644 --- a/app/assets/javascripts/dirty_submit/dirty_submit_collection.js +++ b/app/assets/javascripts/dirty_submit/dirty_submit_collection.js @@ -6,7 +6,7 @@ class DirtySubmitCollection { this.dirtySubmits = []; - this.forms.forEach(form => this.dirtySubmits.push(new DirtySubmitForm(form))); + this.forms.forEach((form) => this.dirtySubmits.push(new DirtySubmitForm(form))); } } diff --git a/app/assets/javascripts/dirty_submit/dirty_submit_form.js b/app/assets/javascripts/dirty_submit/dirty_submit_form.js index 903c31cb0d1..54fd5f91194 100644 --- a/app/assets/javascripts/dirty_submit/dirty_submit_form.js +++ b/app/assets/javascripts/dirty_submit/dirty_submit_form.js @@ -22,10 +22,10 @@ class DirtySubmitForm { registerListeners() { const getThrottledHandlerForInput = memoize(() => - throttle(event => this.updateDirtyInput(event), DirtySubmitForm.THROTTLE_DURATION), + throttle((event) => this.updateDirtyInput(event), DirtySubmitForm.THROTTLE_DURATION), ); - const throttledUpdateDirtyInput = event => { + const throttledUpdateDirtyInput = (event) => { const throttledHandler = getThrottledHandlerForInput(event.target.name); throttledHandler(event); }; @@ -33,7 +33,7 @@ class DirtySubmitForm { this.form.addEventListener('input', throttledUpdateDirtyInput); this.form.addEventListener('change', throttledUpdateDirtyInput); $(this.form).on('change.select2', throttledUpdateDirtyInput); - this.form.addEventListener('submit', event => this.formSubmit(event)); + this.form.addEventListener('submit', (event) => this.formSubmit(event)); } updateDirtyInput(event) { @@ -58,7 +58,7 @@ class DirtySubmitForm { toggleSubmission() { this.isDisabled = this.dirtyInputs.length === 0; - this.submits.forEach(element => { + this.submits.forEach((element) => { element.disabled = this.isDisabled; }); } diff --git a/app/assets/javascripts/droplab/drop_down.js b/app/assets/javascripts/droplab/drop_down.js index 31d32fb5060..f4a0b3ed727 100644 --- a/app/assets/javascripts/droplab/drop_down.js +++ b/app/assets/javascripts/droplab/drop_down.js @@ -68,7 +68,7 @@ class DropDown { removeSelectedClasses() { const items = this.items || this.getItems(); - items.forEach(item => item.classList.remove(SELECTED_CLASS)); + items.forEach((item) => item.classList.remove(SELECTED_CLASS)); } addEvents() { @@ -162,7 +162,7 @@ class DropDown { static setImagesSrc(template) { const images = [...template.querySelectorAll('img[data-src]')]; - images.forEach(image => { + images.forEach((image) => { const img = image; img.src = img.getAttribute('data-src'); diff --git a/app/assets/javascripts/droplab/drop_lab.js b/app/assets/javascripts/droplab/drop_lab.js index 33c05404493..537a05aebb9 100644 --- a/app/assets/javascripts/droplab/drop_lab.js +++ b/app/assets/javascripts/droplab/drop_lab.js @@ -28,7 +28,7 @@ class DropLab { } destroy() { - this.hooks.forEach(hook => hook.destroy()); + this.hooks.forEach((hook) => hook.destroy()); this.hooks = []; this.removeEvents(); } @@ -51,7 +51,7 @@ class DropLab { } processData(trigger, data, methodName) { - this.hooks.forEach(hook => { + this.hooks.forEach((hook) => { if (Array.isArray(trigger)) hook.list[methodName](trigger); if (hook.trigger.id === trigger) hook.list[methodName](data); @@ -70,7 +70,7 @@ class DropLab { if (utils.isDropDownParts(thisTag, this.hooks)) return; if (utils.isDropDownParts(e.target, this.hooks)) return; - this.hooks.forEach(hook => hook.list.hide()); + this.hooks.forEach((hook) => hook.list.hide()); } removeEvents() { @@ -115,7 +115,7 @@ class DropLab { } addHooks(hooks, plugins, config) { - hooks.forEach(hook => this.addHook(hook, null, plugins, config)); + hooks.forEach((hook) => this.addHook(hook, null, plugins, config)); return this; } @@ -147,7 +147,7 @@ class DropLab { this.fireReady(); - this.queuedData.forEach(data => this.addData(data)); + this.queuedData.forEach((data) => this.addData(data)); this.queuedData = []; return this; diff --git a/app/assets/javascripts/droplab/hook_button.js b/app/assets/javascripts/droplab/hook_button.js index af45eba74e7..c58d0052251 100644 --- a/app/assets/javascripts/droplab/hook_button.js +++ b/app/assets/javascripts/droplab/hook_button.js @@ -14,7 +14,7 @@ class HookButton extends Hook { } addPlugins() { - this.plugins.forEach(plugin => plugin.init(this)); + this.plugins.forEach((plugin) => plugin.init(this)); } clicked(e) { @@ -44,7 +44,7 @@ class HookButton extends Hook { } removePlugins() { - this.plugins.forEach(plugin => plugin.destroy()); + this.plugins.forEach((plugin) => plugin.destroy()); } destroy() { diff --git a/app/assets/javascripts/droplab/hook_input.js b/app/assets/javascripts/droplab/hook_input.js index 19131a64f2c..c523dae347f 100644 --- a/app/assets/javascripts/droplab/hook_input.js +++ b/app/assets/javascripts/droplab/hook_input.js @@ -14,7 +14,7 @@ class HookInput extends Hook { } addPlugins() { - this.plugins.forEach(plugin => plugin.init(this)); + this.plugins.forEach((plugin) => plugin.init(this)); } addEvents() { @@ -101,7 +101,7 @@ class HookInput extends Hook { } removePlugins() { - this.plugins.forEach(plugin => plugin.destroy()); + this.plugins.forEach((plugin) => plugin.destroy()); } destroy() { diff --git a/app/assets/javascripts/droplab/keyboard.js b/app/assets/javascripts/droplab/keyboard.js index 40837ffdf8f..fe1ea2fa6b0 100644 --- a/app/assets/javascripts/droplab/keyboard.js +++ b/app/assets/javascripts/droplab/keyboard.js @@ -2,7 +2,7 @@ import { ACTIVE_CLASS } from './constants'; -const Keyboard = function() { +const Keyboard = function () { var currentKey; var currentFocus; var isUpArrow = false; diff --git a/app/assets/javascripts/droplab/plugins/ajax.js b/app/assets/javascripts/droplab/plugins/ajax.js index 48b2a90c459..77d60454d1a 100644 --- a/app/assets/javascripts/droplab/plugins/ajax.js +++ b/app/assets/javascripts/droplab/plugins/ajax.js @@ -43,10 +43,10 @@ const Ajax = { return AjaxCache.retrieve(config.endpoint) .then(self.preprocessing.bind(null, config)) - .then(data => self._loadData(data, config, self)) + .then((data) => self._loadData(data, config, self)) .catch(config.onError); }, - destroy: function() { + destroy: function () { this.destroyed = true; }, }; diff --git a/app/assets/javascripts/droplab/plugins/ajax_filter.js b/app/assets/javascripts/droplab/plugins/ajax_filter.js index 66a52548417..ac4d44adc17 100644 --- a/app/assets/javascripts/droplab/plugins/ajax_filter.js +++ b/app/assets/javascripts/droplab/plugins/ajax_filter.js @@ -2,7 +2,7 @@ import AjaxCache from '../../lib/utils/ajax_cache'; const AjaxFilter = { - init: function(hook) { + init: function (hook) { this.destroyed = false; this.hook = hook; this.notLoading(); @@ -63,7 +63,7 @@ const AjaxFilter = { params[config.searchKey] = searchValue; var url = config.endpoint + this.buildParams(params); return AjaxCache.retrieve(url) - .then(data => { + .then((data) => { this._loadData(data, config); if (config.onLoadingFinished) { config.onLoadingFinished(data); @@ -93,9 +93,9 @@ const AjaxFilter = { list.currentIndex = 0; }, - buildParams: function(params) { + buildParams: function (params) { if (!params) return ''; - var paramsArray = Object.keys(params).map(function(param) { + var paramsArray = Object.keys(params).map(function (param) { return param + '=' + (params[param] || ''); }); return '?' + paramsArray.join('&'); diff --git a/app/assets/javascripts/droplab/plugins/filter.js b/app/assets/javascripts/droplab/plugins/filter.js index 6f1dc252d24..06391668928 100644 --- a/app/assets/javascripts/droplab/plugins/filter.js +++ b/app/assets/javascripts/droplab/plugins/filter.js @@ -1,7 +1,7 @@ /* eslint-disable */ const Filter = { - keydown: function(e) { + keydown: function (e) { if (this.destroyed) return; var hiddenCount = 0; @@ -21,22 +21,22 @@ const Filter = { if (config && config.filterFunction && typeof config.filterFunction === 'function') { filterFunction = config.filterFunction; } else { - filterFunction = function(o) { + filterFunction = function (o) { // cheap string search o.droplab_hidden = o[config.template].toLowerCase().indexOf(value) === -1; return o; }; } - dataHiddenCount = data.filter(function(o) { + dataHiddenCount = data.filter(function (o) { return !o.droplab_hidden; }).length; - matches = data.map(function(o) { + matches = data.map(function (o) { return filterFunction(o, value); }); - hiddenCount = matches.filter(function(o) { + hiddenCount = matches.filter(function (o) { return !o.droplab_hidden; }).length; diff --git a/app/assets/javascripts/droplab/plugins/input_setter.js b/app/assets/javascripts/droplab/plugins/input_setter.js index 6cfc738a1e3..148d9a35b81 100644 --- a/app/assets/javascripts/droplab/plugins/input_setter.js +++ b/app/assets/javascripts/droplab/plugins/input_setter.js @@ -27,7 +27,7 @@ const InputSetter = { if (!Array.isArray(this.config)) this.config = [this.config]; - this.config.forEach(config => this.setInput(config, selectedItem)); + this.config.forEach((config) => this.setInput(config, selectedItem)); }, setInput(config, selectedItem) { diff --git a/app/assets/javascripts/droplab/utils.js b/app/assets/javascripts/droplab/utils.js index df3c5c2132a..d7f49bf19d8 100644 --- a/app/assets/javascripts/droplab/utils.js +++ b/app/assets/javascripts/droplab/utils.js @@ -5,12 +5,7 @@ import { DATA_TRIGGER, DATA_DROPDOWN, TEMPLATE_REGEX } from './constants'; const utils = { toCamelCase(attr) { - return this.camelize( - attr - .split('-') - .slice(1) - .join(' '), - ); + return this.camelize(attr.split('-').slice(1).join(' ')); }, template(templateString, data) { diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 69961d2e07a..d7aacfbce60 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -46,7 +46,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) { let uploadFile; formTextarea.wrap('<div class="div-dropzone"></div>'); - formTextarea.on('paste', event => handlePaste(event)); + formTextarea.on('paste', (event) => handlePaste(event)); // Add dropzone area to the form. const $mdArea = formTextarea.closest('.md-area'); @@ -139,7 +139,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) { // removeAllFiles(true) stops uploading files (if any) // and remove them from dropzone files queue. - $cancelButton.on('click', e => { + $cancelButton.on('click', (e) => { e.preventDefault(); e.stopPropagation(); Dropzone.forElement($formDropzone.get(0)).removeAllFiles(true); @@ -149,7 +149,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) { // clear dropzone files queue, change status of failed files to undefined, // and add that files to the dropzone files queue again. // addFile() adds file to dropzone files queue and upload it. - $retryLink.on('click', e => { + $retryLink.on('click', (e) => { const dropzoneInstance = Dropzone.forElement( e.target.closest('.js-main-target-form').querySelector('.div-dropzone'), ); @@ -161,7 +161,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) { // uploading of files that are being uploaded at the moment. dropzoneInstance.removeAllFiles(true); - failedFiles.map(failedFile => { + failedFiles.map((failedFile) => { const file = failedFile; if (file.status === Dropzone.ERROR) { @@ -173,7 +173,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) { }); }); // eslint-disable-next-line consistent-return - handlePaste = event => { + handlePaste = (event) => { const pasteEvent = event.originalEvent; const { clipboardData } = pasteEvent; if (clipboardData && clipboardData.items) { @@ -198,7 +198,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) { } }; - isImage = data => { + isImage = (data) => { let i = 0; while (i < data.clipboardData.items.length) { const item = data.clipboardData.items[i]; @@ -219,12 +219,8 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) { const caretStart = textarea.selectionStart; const caretEnd = textarea.selectionEnd; const textEnd = $(child).val().length; - const beforeSelection = $(child) - .val() - .substring(0, caretStart); - const afterSelection = $(child) - .val() - .substring(caretEnd, textEnd); + const beforeSelection = $(child).val().substring(0, caretStart); + const afterSelection = $(child).val().substring(caretEnd, textEnd); $(child).val(beforeSelection + formattedText + afterSelection); textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length); textarea.style.height = `${textarea.scrollHeight}px`; @@ -232,7 +228,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) { return formTextarea.trigger('input'); }; - addFileToForm = path => { + addFileToForm = (path) => { $(form).append(`<input type="hidden" name="files[]" value="${escape(path)}">`); }; @@ -240,7 +236,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) { const closeSpinner = () => $uploadingProgressContainer.addClass('hide'); - const showError = message => { + const showError = (message) => { $uploadingErrorContainer.removeClass('hide'); $uploadingErrorMessage.html(message); }; @@ -273,15 +269,16 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) { insertToTextArea(filename, md); closeSpinner(); }) - .catch(e => { + .catch((e) => { showError(e.response.data.message); closeSpinner(); }); }; updateAttachingMessage = (files, messageContainer) => { - const filesCount = files.filter(file => file.status === 'uploading' || file.status === 'queued') - .length; + const filesCount = files.filter( + (file) => file.status === 'uploading' || file.status === 'queued', + ).length; const attachingMessage = n__('Attaching a file', 'Attaching %d files', filesCount); messageContainer.text(`${attachingMessage} -`); @@ -289,10 +286,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) { form.find('.markdown-selector').click(function onMarkdownClick(e) { e.preventDefault(); - $(this) - .closest('.gfm-form') - .find('.div-dropzone') - .click(); + $(this).closest('.gfm-form').find('.div-dropzone').click(); formTextarea.focus(); }); diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index ffb5232ca75..c311e1b561c 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -55,9 +55,9 @@ class DueDateSelect { field: $dueDateInput.get(0), theme: 'gitlab-theme', format: 'yyyy-mm-dd', - parse: dateString => parsePikadayDate(dateString), - toString: date => pikadayToString(date), - onSelect: dateText => { + parse: (dateString) => parsePikadayDate(dateString), + toString: (date) => pikadayToString(date), + onSelect: (dateText) => { $dueDateInput.val(calendar.toString(dateText)); if (this.$dropdown.hasClass('js-issue-boards-due-date')) { @@ -76,7 +76,7 @@ class DueDateSelect { } initRemoveDueDate() { - this.$block.on('click', '.js-remove-due-date', e => { + this.$block.on('click', '.js-remove-due-date', (e) => { const calendar = this.$datePicker.data('pikaday'); e.preventDefault(); @@ -103,7 +103,7 @@ class DueDateSelect { if (this.rawSelectedDate.length) { // Construct Date object manually to avoid buggy dateString support within Date constructor - const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10)); + const dateArray = this.rawSelectedDate.split('-').map((v) => parseInt(v, 10)); const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]); this.displayedDate = dateFormat(dateObj, 'mmm d, yyyy'); } else { @@ -182,8 +182,8 @@ export default class DueDateSelectors { theme: 'gitlab-theme animate-picker', format: 'yyyy-mm-dd', container: $datePicker.parent().get(0), - parse: dateString => parsePikadayDate(dateString), - toString: date => pikadayToString(date), + parse: (dateString) => parsePikadayDate(dateString), + toString: (date) => pikadayToString(date), onSelect(dateText) { $datePicker.val(calendar.toString(dateText)); }, @@ -195,11 +195,9 @@ export default class DueDateSelectors { $datePicker.data('pikaday', calendar); }); - $('.js-clear-due-date,.js-clear-start-date').on('click', e => { + $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => { e.preventDefault(); - const calendar = $(e.target) - .siblings('.datepicker') - .data('pikaday'); + const calendar = $(e.target).siblings('.datepicker').data('pikaday'); calendar.setDate(null); }); } diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js index d6f87872bde..8d1a3d17c6e 100644 --- a/app/assets/javascripts/editor/constants.js +++ b/app/assets/javascripts/editor/constants.js @@ -10,3 +10,12 @@ export const CONTENT_UPDATE_DEBOUNCE = 250; export const ERROR_INSTANCE_REQUIRED_FOR_EXTENSION = __( 'Editor Lite instance is required to set up an extension.', ); + +// +// EXTENSIONS' CONSTANTS +// + +// For CI config schemas the filename must match +// '*.gitlab-ci.yml' regardless of project configuration. +// https://gitlab.com/gitlab-org/gitlab/-/issues/293641 +export const EXTENSION_CI_SCHEMA_FILE_NAME_MATCH = '.gitlab-ci.yml'; diff --git a/app/assets/javascripts/editor/editor_lite.js b/app/assets/javascripts/editor/editor_lite.js index 2bd1cdc84d0..1808f968b8c 100644 --- a/app/assets/javascripts/editor/editor_lite.js +++ b/app/assets/javascripts/editor/editor_lite.js @@ -24,7 +24,7 @@ export default class EditorLite { static setupMonacoTheme() { const themeName = window.gon?.user_color_scheme || DEFAULT_THEME; - const theme = themes.find(t => t.name === themeName); + const theme = themes.find((t) => t.name === themeName); if (theme) monacoEditor.defineTheme(themeName, theme.data); monacoEditor.setTheme(theme ? themeName : DEFAULT_THEME); } @@ -35,7 +35,7 @@ export default class EditorLite { const ext = `.${path.split('.').pop()}`; const language = monacoLanguages .getLanguages() - .find(lang => lang.extensions.indexOf(ext) !== -1); + .find((lang) => lang.extensions.indexOf(ext) !== -1); const id = language ? language.id : 'plaintext'; monacoEditor.setModelLanguage(model, id); } @@ -51,7 +51,7 @@ export default class EditorLite { const promises = []; const extensionsArray = typeof extensions === 'string' ? extensions.split(',') : extensions; - extensionsArray.forEach(ext => { + extensionsArray.forEach((ext) => { const prefix = ext.includes('/') ? '' : 'editor/'; const trimmedExt = ext.replace(/^\//, '').trim(); EditorLite.pushToImportsArray(promises, `~/${prefix}${trimmedExt}`); @@ -66,7 +66,7 @@ export default class EditorLite { } const isClassInstance = source.constructor.prototype !== Object.prototype; const sanitizedSource = isClassInstance ? source.constructor.prototype : source; - Object.getOwnPropertyNames(sanitizedSource).forEach(prop => { + Object.getOwnPropertyNames(sanitizedSource).forEach((prop) => { if (prop !== 'constructor') { Object.assign(inst, { [prop]: source[prop] }); } @@ -110,17 +110,17 @@ export default class EditorLite { }); instance.setModel(model); instance.onDidDispose(() => { - const index = this.instances.findIndex(inst => inst === instance); + const index = this.instances.findIndex((inst) => inst === instance); this.instances.splice(index, 1); model.dispose(); }); - instance.updateModelLanguage = path => EditorLite.updateModelLanguage(path, instance); - instance.use = args => this.use(args, instance); + instance.updateModelLanguage = (path) => EditorLite.updateModelLanguage(path, instance); + instance.use = (args) => this.use(args, instance); EditorLite.loadExtensions(extensions, instance) - .then(modules => { + .then((modules) => { if (modules) { - modules.forEach(module => { + modules.forEach((module) => { instance.use(module.default); }); } @@ -128,7 +128,7 @@ export default class EditorLite { .then(() => { el.dispatchEvent(new Event('editor-ready')); }) - .catch(e => { + .catch((e) => { throw e; }); @@ -137,20 +137,20 @@ export default class EditorLite { } dispose() { - this.instances.forEach(instance => instance.dispose()); + this.instances.forEach((instance) => instance.dispose()); } use(exts = [], instance = null) { const extensions = Array.isArray(exts) ? exts : [exts]; - const initExtensions = inst => { - extensions.forEach(extension => { + const initExtensions = (inst) => { + extensions.forEach((extension) => { EditorLite.mixIntoInstance(extension, inst); }); }; if (instance) { initExtensions(instance); } else { - this.instances.forEach(inst => { + this.instances.forEach((inst) => { initExtensions(inst); }); } diff --git a/app/assets/javascripts/editor/extensions/editor_ci_schema_ext.js b/app/assets/javascripts/editor/extensions/editor_ci_schema_ext.js new file mode 100644 index 00000000000..eb47c20912e --- /dev/null +++ b/app/assets/javascripts/editor/extensions/editor_ci_schema_ext.js @@ -0,0 +1,38 @@ +import Api from '~/api'; +import { registerSchema } from '~/ide/utils'; +import { EditorLiteExtension } from './editor_lite_extension_base'; +import { EXTENSION_CI_SCHEMA_FILE_NAME_MATCH } from '../constants'; + +export class CiSchemaExtension extends EditorLiteExtension { + /** + * Registers a syntax schema to the editor based on project + * identifier and commit. + * + * The schema is added to the file that is currently edited + * in the editor. + * + * @param {Object} opts + * @param {String} opts.projectNamespace + * @param {String} opts.projectPath + * @param {String?} opts.ref - Current ref. Defaults to master + */ + registerCiSchema({ projectNamespace, projectPath, ref = 'master' } = {}) { + const ciSchemaPath = Api.buildUrl(Api.projectFileSchemaPath) + .replace(':namespace_path', projectNamespace) + .replace(':project_path', projectPath) + .replace(':ref', ref) + .replace(':filename', EXTENSION_CI_SCHEMA_FILE_NAME_MATCH); + // In order for workers loaded from `data://` as the + // ones loaded by monaco editor, we use absolute URLs + // to fetch schema files, hence the `gon.gitlab_url` + // reference. This prevents error: + // "Failed to execute 'fetch' on 'WorkerGlobalScope'" + const absoluteSchemaUrl = gon.gitlab_url + ciSchemaPath; + const modelFileName = this.getModel().uri.path.split('/').pop(); + + registerSchema({ + uri: absoluteSchemaUrl, + fileMatch: [modelFileName], + }); + } +} diff --git a/app/assets/javascripts/editor/editor_file_template_ext.js b/app/assets/javascripts/editor/extensions/editor_file_template_ext.js index f5474318447..f5474318447 100644 --- a/app/assets/javascripts/editor/editor_file_template_ext.js +++ b/app/assets/javascripts/editor/extensions/editor_file_template_ext.js diff --git a/app/assets/javascripts/editor/editor_lite_extension_base.js b/app/assets/javascripts/editor/extensions/editor_lite_extension_base.js index b8d87fa4969..8d350068973 100644 --- a/app/assets/javascripts/editor/editor_lite_extension_base.js +++ b/app/assets/javascripts/editor/extensions/editor_lite_extension_base.js @@ -1,4 +1,4 @@ -import { ERROR_INSTANCE_REQUIRED_FOR_EXTENSION } from './constants'; +import { ERROR_INSTANCE_REQUIRED_FOR_EXTENSION } from '../constants'; export class EditorLiteExtension { constructor({ instance, ...options } = {}) { diff --git a/app/assets/javascripts/editor/editor_markdown_ext.js b/app/assets/javascripts/editor/extensions/editor_markdown_ext.js index 19e0037c175..2ce003753f7 100644 --- a/app/assets/javascripts/editor/editor_markdown_ext.js +++ b/app/assets/javascripts/editor/extensions/editor_markdown_ext.js @@ -76,7 +76,7 @@ export class EditorMarkdownExtension extends EditorLiteExtension { if (textLines.length > 1) { // Multi-line selection - lineShift = textLines.findIndex(line => line.indexOf(toSelect) !== -1); + lineShift = textLines.findIndex((line) => line.indexOf(toSelect) !== -1); newStartLineNumber = currentSelection.startLineNumber + lineShift; newStartColumn = textLines[lineShift].indexOf(toSelect) + 1; } else { diff --git a/app/assets/javascripts/editor/utils.js b/app/assets/javascripts/editor/utils.js index d8b6396b671..af4473413f4 100644 --- a/app/assets/javascripts/editor/utils.js +++ b/app/assets/javascripts/editor/utils.js @@ -1,4 +1,4 @@ -export const clearDomElement = el => { +export const clearDomElement = (el) => { if (!el || !el.firstChild) return; while (el.firstChild) { diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js index 4a56843c0b5..fa1024a74a4 100644 --- a/app/assets/javascripts/emoji/index.js +++ b/app/assets/javascripts/emoji/index.js @@ -35,7 +35,7 @@ async function prepareEmojiMap() { validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)]; - Object.keys(emojiMap).forEach(name => { + Object.keys(emojiMap).forEach((name) => { emojiMap[name].aliases = []; emojiMap[name].name = name; }); @@ -122,23 +122,23 @@ const searchMatchers = { const searchPredicates = { // Search by name - name: (matcher, query) => emoji => { + name: (matcher, query) => (emoji) => { const m = matcher(emoji.name, query); return [{ ...m, emoji, field: emoji.name }]; }, // Search by alias - alias: (matcher, query) => emoji => - emoji.aliases.map(alias => { + alias: (matcher, query) => (emoji) => + emoji.aliases.map((alias) => { const m = matcher(alias, query); return { ...m, emoji, field: alias }; }), // Search by description - description: (matcher, query) => emoji => { + description: (matcher, query) => (emoji) => { const m = matcher(emoji.d, query); return [{ ...m, emoji, field: emoji.d }]; }, // Search by unicode value (always exact) - unicode: (matcher, query) => emoji => { + unicode: (matcher, query) => (emoji) => { return [{ emoji, field: emoji.e, success: emoji.e === query }]; }, }; @@ -196,18 +196,18 @@ export function searchEmoji(query, opts) { } const matcher = searchMatchers[match] || searchMatchers.exact; - const predicates = fields.map(f => searchPredicates[f](matcher, query)); + const predicates = fields.map((f) => searchPredicates[f](matcher, query)); const results = Object.values(emojiMap) - .flatMap(emoji => predicates.flatMap(predicate => predicate(emoji))) - .filter(r => r.success); + .flatMap((emoji) => predicates.flatMap((predicate) => predicate(emoji))) + .filter((r) => r.success); // Fallback to question mark for unknown emojis if (fallback && results.length === 0) { return raw ? [{ emoji: fallbackEmoji }] : [fallbackEmoji]; } - return raw ? results : results.map(r => r.emoji); + return raw ? results : results.map((r) => r.emoji); } let emojiCategoryMap; @@ -223,7 +223,7 @@ export function getEmojiCategoryMap() { symbols: [], flags: [], }; - Object.keys(emojiMap).forEach(name => { + Object.keys(emojiMap).forEach((name) => { const emoji = emojiMap[name]; if (emojiCategoryMap[emoji.c]) { emojiCategoryMap[emoji.c].push(name); @@ -242,8 +242,9 @@ export function getEmojiInfo(query) { export function emojiFallbackImageSrc(inputName) { const { name } = getEmojiInfo(inputName); - return `${gon.asset_host || ''}${gon.relative_url_root || - ''}/-/emojis/${EMOJI_VERSION}/${name}.png`; + return `${gon.asset_host || ''}${ + gon.relative_url_root || '' + }/-/emojis/${EMOJI_VERSION}/${name}.png`; } export function emojiImageTag(name, src) { diff --git a/app/assets/javascripts/emoji/no_emoji_validator.js b/app/assets/javascripts/emoji/no_emoji_validator.js index edef868619a..85c8204225a 100644 --- a/app/assets/javascripts/emoji/no_emoji_validator.js +++ b/app/assets/javascripts/emoji/no_emoji_validator.js @@ -9,7 +9,7 @@ export default class NoEmojiValidator extends InputValidator { const container = opts.container || ''; this.noEmojiEmelents = document.querySelectorAll(`${container} .js-block-emoji`); - this.noEmojiEmelents.forEach(element => + this.noEmojiEmelents.forEach((element) => element.addEventListener('input', this.eventHandler.bind(this)), ); } diff --git a/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js index c5f9fcf6358..cf9794e6a87 100644 --- a/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js +++ b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js @@ -33,7 +33,7 @@ const tone5 = 127999; // parseInt('1F3FF', 16) function isSkinToneComboEmoji(emojiUnicode) { return ( emojiUnicode.length > 2 && - Array.from(emojiUnicode).some(char => { + Array.from(emojiUnicode).some((char) => { const cp = char.codePointAt(0); return cp >= tone1 && cp <= tone5; }) @@ -60,7 +60,7 @@ const personEndCodePoint = 128105; // parseInt('1F469', 16) function isPersonZwjEmoji(emojiUnicode) { let hasPersonEmoji = false; let hasZwj = false; - Array.from(emojiUnicode).forEach(character => { + Array.from(emojiUnicode).forEach((character) => { const cp = character.codePointAt(0); if (cp === zwj) { hasZwj = true; diff --git a/app/assets/javascripts/emoji/support/unicode_support_map.js b/app/assets/javascripts/emoji/support/unicode_support_map.js index 651169391fe..fe3bc75f9fd 100644 --- a/app/assets/javascripts/emoji/support/unicode_support_map.js +++ b/app/assets/javascripts/emoji/support/unicode_support_map.js @@ -89,9 +89,9 @@ function generateUnicodeSupportMap(testMap) { ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`; // Write each emoji to the canvas vertically let writeIndex = 0; - testMapKeys.forEach(testKey => { + testMapKeys.forEach((testKey) => { const testEntry = testMap[testKey]; - [].concat(testEntry).forEach(emojiUnicode => { + [].concat(testEntry).forEach((emojiUnicode) => { ctx.fillText(emojiUnicode, 0, writeIndex * fontSize + fontSize / 2); writeIndex += 1; }); @@ -100,11 +100,11 @@ function generateUnicodeSupportMap(testMap) { // Read from the canvas const resultMap = {}; let readIndex = 0; - testMapKeys.forEach(testKey => { + testMapKeys.forEach((testKey) => { const testEntry = testMap[testKey]; // This needs to be a `reduce` instead of `every` because we need to // keep the `readIndex` in sync from the writes by running all entries - const isTestSatisfied = [].concat(testEntry).reduce(isSatisfied => { + const isTestSatisfied = [].concat(testEntry).reduce((isSatisfied) => { // Sample along the vertical-middle for a couple of characters const imageData = ctx.getImageData(0, readIndex * fontSize + fontSize / 2, 2 * fontSize, 1) .data; diff --git a/app/assets/javascripts/environments/components/canary_ingress.vue b/app/assets/javascripts/environments/components/canary_ingress.vue new file mode 100644 index 00000000000..f8cdbb96bc2 --- /dev/null +++ b/app/assets/javascripts/environments/components/canary_ingress.vue @@ -0,0 +1,109 @@ +<script> +import { uniqueId } from 'lodash'; +import { GlDropdown, GlDropdownItem, GlModalDirective as GlModal } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { CANARY_UPDATE_MODAL } from '../constants'; + +export default { + components: { + GlDropdown, + GlDropdownItem, + }, + directives: { + GlModal, + }, + props: { + canaryIngress: { + required: true, + type: Object, + }, + }, + ingressOptions: Array(100 / 5 + 1) + .fill(0) + .map((_, i) => i * 5), + + translations: { + stableLabel: s__('CanaryIngress|Stable'), + canaryLabel: s__('CanaryIngress|Canary'), + }, + + CANARY_UPDATE_MODAL, + + css: { + label: [ + 'gl-font-base', + 'gl-font-weight-normal', + 'gl-line-height-normal', + 'gl-inset-border-1-gray-200', + 'gl-py-3', + 'gl-px-4', + 'gl-mb-0', + ], + }, + computed: { + stableWeightId() { + return uniqueId('stable-weight-'); + }, + canaryWeightId() { + return uniqueId('canary-weight-'); + }, + stableWeight() { + return (100 - this.canaryIngress.canary_weight).toString(); + }, + canaryWeight() { + return this.canaryIngress.canary_weight.toString(); + }, + }, + methods: { + changeCanary(weight) { + this.$emit('change', weight); + }, + changeStable(weight) { + this.$emit('change', 100 - weight); + }, + }, +}; +</script> +<template> + <section class="gl-display-flex gl-bg-white gl-m-3"> + <div class="gl-display-flex gl-flex-direction-column"> + <label :for="stableWeightId" :class="$options.css.label" class="gl-rounded-top-left-base"> + {{ $options.translations.stableLabel }} + </label> + <gl-dropdown + :id="stableWeightId" + :text="stableWeight" + data-testid="stable-weight" + class="gl-w-full" + toggle-class="gl-rounded-top-left-none! gl-rounded-top-right-none! gl-rounded-bottom-right-none!" + > + <gl-dropdown-item + v-for="option in $options.ingressOptions" + :key="option" + v-gl-modal="$options.CANARY_UPDATE_MODAL" + @click="changeStable(option)" + >{{ option }}</gl-dropdown-item + > + </gl-dropdown> + </div> + <div class="gl-display-flex gl-display-flex gl-flex-direction-column"> + <label :for="canaryWeightId" :class="$options.css.label" class="gl-rounded-top-right-base">{{ + $options.translations.canaryLabel + }}</label> + <gl-dropdown + :id="canaryWeightId" + :text="canaryWeight" + data-testid="canary-weight" + toggle-class="gl-rounded-top-left-none! gl-rounded-top-right-none! gl-rounded-bottom-left-none! gl-border-l-none!" + > + <gl-dropdown-item + v-for="option in $options.ingressOptions" + :key="option" + v-gl-modal="$options.CANARY_UPDATE_MODAL" + @click="changeCanary(option)" + >{{ option }}</gl-dropdown-item + > + </gl-dropdown> + </div> + </section> +</template> diff --git a/app/assets/javascripts/environments/components/canary_update_modal.vue b/app/assets/javascripts/environments/components/canary_update_modal.vue new file mode 100644 index 00000000000..fc63d6272c8 --- /dev/null +++ b/app/assets/javascripts/environments/components/canary_update_modal.vue @@ -0,0 +1,133 @@ +<script> +import { GlAlert, GlModal, GlSprintf } from '@gitlab/ui'; +import { __, s__ } from '~/locale'; +import updateCanaryIngress from '../graphql/mutations/update_canary_ingress.mutation.graphql'; +import { CANARY_UPDATE_MODAL } from '../constants'; + +export default { + components: { + GlAlert, + GlModal, + GlSprintf, + }, + props: { + environment: { + type: Object, + required: false, + default: () => ({}), + }, + weight: { + type: Number, + required: false, + default: 0, + }, + visible: { + type: Boolean, + required: false, + default: false, + }, + }, + translations: { + title: s__('CanaryIngress|Change the ratio of canary deployments?'), + ratioChange: s__( + 'CanaryIngress|You are changing the ratio of the canary rollout for %{environment} compared to the stable deployment to:', + ), + stableWeight: s__('CanaryIngress|%{boldStart}Stable:%{boldEnd} %{stable}'), + canaryWeight: s__('CanaryIngress|%{boldStart}Canary:%{boldEnd} %{canary}'), + deploymentWarning: s__( + 'CanaryIngress|Doing so will set a deployment change in progress. This temporarily blocks any further configuration until the deployment is finished.', + ), + }, + modal: { + modalId: CANARY_UPDATE_MODAL, + actionPrimary: { + text: s__('CanaryIngress|Change ratio'), + attributes: [{ variant: 'info' }], + }, + actionCancel: { text: __('Cancel') }, + static: true, + }, + data() { + return { error: '', dismissed: true }; + }, + computed: { + stableWeight() { + return (100 - this.weight).toString(); + }, + canaryWeight() { + return this.weight.toString(); + }, + hasError() { + return Boolean(this.error); + }, + environmentName() { + return this.environment?.name ?? ''; + }, + }, + methods: { + submitCanaryChange() { + return this.$apollo + .mutate({ + mutation: updateCanaryIngress, + variables: { + input: { + id: this.environment.global_id, + weight: this.weight, + }, + }, + }) + .then( + ({ + data: { + environmentsCanaryIngressUpdate: { + errors: [error], + }, + }, + }) => { + this.error = error; + }, + ) + .catch(() => { + this.error = __('Something went wrong. Please try again later'); + }); + }, + dismiss() { + this.error = ''; + }, + }, +}; +</script> +<template> + <div> + <gl-alert v-if="hasError" variant="danger" @dismiss="dismiss">{{ error }}</gl-alert> + <gl-modal v-bind="$options.modal" :visible="visible" @primary="submitCanaryChange"> + <template #modal-title>{{ $options.translations.title }}</template> + <template #default> + <p> + <gl-sprintf :message="$options.translations.ratioChange"> + <template #environment>{{ environmentName }}</template> + </gl-sprintf> + </p> + <ul class="gl-list-style-none gl-p-0"> + <li> + <gl-sprintf :message="$options.translations.stableWeight"> + <template #bold="{ content }"> + <span class="gl-font-weight-bold">{{ content }}</span> + </template> + <template #stable>{{ stableWeight }}</template> + </gl-sprintf> + </li> + <li> + <gl-sprintf :message="$options.translations.canaryWeight"> + <template #bold="{ content }"> + <span class="gl-font-weight-bold">{{ content }}</span> + </template> + <template #canary>{{ canaryWeight }}</template> + </gl-sprintf> + </li> + </ul> + <p>{{ $options.translations.deploymentWarning }}</p> + </template> + </gl-modal> + </div> +</template> diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue index e7697f14802..c6b34fecbb7 100644 --- a/app/assets/javascripts/environments/components/container.vue +++ b/app/assets/javascripts/environments/components/container.vue @@ -10,11 +10,6 @@ export default { GlLoadingIcon, }, props: { - canaryDeploymentFeatureId: { - type: String, - required: false, - default: null, - }, isLoading: { type: Boolean, required: true, @@ -46,11 +41,6 @@ export default { required: false, default: '', }, - showCanaryDeploymentCallout: { - type: Boolean, - required: false, - default: false, - }, userCalloutsPath: { type: String, required: false, @@ -75,8 +65,6 @@ export default { <environment-table :environments="environments" :can-read-environment="canReadEnvironment" - :canary-deployment-feature-id="canaryDeploymentFeatureId" - :show-canary-deployment-callout="showCanaryDeploymentCallout" :user-callouts-path="userCalloutsPath" :lock-promotion-svg-path="lockPromotionSvgPath" :help-canary-deployments-path="helpCanaryDeploymentsPath" diff --git a/app/assets/javascripts/environments/components/deploy_board.vue b/app/assets/javascripts/environments/components/deploy_board.vue new file mode 100644 index 00000000000..07cb968d8d3 --- /dev/null +++ b/app/assets/javascripts/environments/components/deploy_board.vue @@ -0,0 +1,216 @@ +<script> +/* eslint-disable @gitlab/vue-require-i18n-strings */ +/** + * Renders a deploy board. + * + * A deploy board is composed by: + * - Information area with percentage of completion. + * - Instances with status. + * - Button Actions. + * [Mockup](https://gitlab.com/gitlab-org/gitlab-foss/uploads/2f655655c0eadf655d0ae7467b53002a/environments__deploy-graphic.png) + */ +import { isEmpty } from 'lodash'; +import { + GlIcon, + GlLoadingIcon, + GlLink, + GlTooltip, + GlTooltipDirective, + GlSafeHtmlDirective as SafeHtml, +} from '@gitlab/ui'; +import deployBoardSvg from '@gitlab/svgs/dist/illustrations/deploy-boards.svg'; +import instanceComponent from '~/vue_shared/components/deployment_instance.vue'; +import { n__ } from '~/locale'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { STATUS_MAP, CANARY_STATUS } from '../constants'; +import CanaryIngress from './canary_ingress.vue'; + +export default { + components: { + instanceComponent, + CanaryIngress, + GlIcon, + GlLoadingIcon, + GlLink, + GlTooltip, + }, + directives: { + GlTooltip: GlTooltipDirective, + SafeHtml, + }, + mixins: [glFeatureFlagsMixin()], + props: { + deployBoardData: { + type: Object, + required: true, + }, + deployBoardsHelpPath: { + type: String, + required: false, + default: '', + }, + isLoading: { + type: Boolean, + required: true, + }, + isEmpty: { + type: Boolean, + required: true, + }, + logsPath: { + type: String, + required: false, + default: '', + }, + }, + computed: { + canRenderDeployBoard() { + return !this.isEmpty && !isEmpty(this.deployBoardData); + }, + canRenderEmptyState() { + return this.isEmpty; + }, + canRenderCanaryWeight() { + return ( + this.glFeatures.canaryIngressWeightControl && !isEmpty(this.deployBoardData.canary_ingress) + ); + }, + instanceCount() { + const { instances } = this.deployBoardData; + + return Array.isArray(instances) ? instances.length : 0; + }, + instanceIsCompletedCount() { + const completionPercentage = this.deployBoardData.completion / 100; + const completionCount = Math.floor(completionPercentage * this.instanceCount); + + return Number.isNaN(completionCount) ? 0 : completionCount; + }, + instanceIsCompletedText() { + const title = n__('instance completed', 'instances completed', this.instanceIsCompletedCount); + + return `${this.instanceIsCompletedCount} ${title}`; + }, + instanceTitle() { + return n__('Instance', 'Instances', this.instanceCount); + }, + deployBoardSvg() { + return deployBoardSvg; + }, + deployBoardActions() { + return this.deployBoardData.rollback_url || this.deployBoardData.abort_url; + }, + statuses() { + // Canary is not a pod status but it needs to be in the legend. + // Hence adding it here. + return { + ...STATUS_MAP, + CANARY_STATUS, + }; + }, + }, + methods: { + changeCanaryWeight(weight) { + this.$emit('changeCanaryWeight', weight); + }, + }, +}; +</script> +<template> + <div class="js-deploy-board deploy-board"> + <gl-loading-icon v-if="isLoading" class="loading-icon" /> + <template v-else> + <div v-if="canRenderDeployBoard" class="deploy-board-information gl-p-5"> + <div class="deploy-board-information gl-w-full"> + <section class="deploy-board-status"> + <span v-gl-tooltip :title="instanceIsCompletedText"> + <span ref="percentage" class="gl-text-center text-plain gl-font-lg" + >{{ deployBoardData.completion }}%</span + > + <span class="text text-center text-secondary">{{ __('Complete') }}</span> + </span> + </section> + + <section class="deploy-board-instances"> + <div class="gl-font-base text-secondary"> + <span class="deploy-board-instances-text" + >{{ instanceTitle }} ({{ instanceCount }})</span + > + <span ref="legend-icon" data-testid="legend-tooltip-target"> + <gl-icon class="gl-text-blue-500 gl-ml-2" name="question" /> + </span> + <gl-tooltip :target="() => $refs['legend-icon']" boundary="#content-body"> + <div class="deploy-board-legend gl-display-flex gl-flex-direction-column"> + <div + v-for="status in statuses" + :key="status.text" + class="gl-display-flex gl-align-items-center" + > + <instance-component :status="status.class" :stable="status.stable" /> + <span class="legend-text gl-ml-3">{{ status.text }}</span> + </div> + </div> + </gl-tooltip> + </div> + + <div class="deploy-board-instances-container d-flex flex-wrap flex-row"> + <template v-for="(instance, i) in deployBoardData.instances"> + <instance-component + :key="i" + :status="instance.status" + :tooltip-text="instance.tooltip" + :pod-name="instance.pod_name" + :logs-path="logsPath" + :stable="instance.stable" + /> + </template> + </div> + </section> + + <canary-ingress + v-if="canRenderCanaryWeight" + class="deploy-board-canary-ingress" + :canary-ingress="deployBoardData.canary_ingress" + @change="changeCanaryWeight" + /> + + <section v-if="deployBoardActions" class="deploy-board-actions"> + <gl-link + v-if="deployBoardData.rollback_url" + :href="deployBoardData.rollback_url" + class="btn" + data-method="post" + rel="nofollow" + >{{ __('Rollback') }}</gl-link + > + <gl-link + v-if="deployBoardData.abort_url" + :href="deployBoardData.abort_url" + class="btn btn-danger btn-inverted" + data-method="post" + rel="nofollow" + >{{ __('Abort') }}</gl-link + > + </section> + </div> + </div> + + <div v-if="canRenderEmptyState" class="deploy-board-empty"> + <section v-safe-html="deployBoardSvg" class="deploy-board-empty-state-svg"></section> + + <section class="deploy-board-empty-state-text"> + <span class="deploy-board-empty-state-title d-flex">{{ + __('Kubernetes deployment not found') + }}</span> + <span> + To see deployment progress for your environments, make sure you are deploying to + <code>$KUBE_NAMESPACE</code> and annotating with + <code>app.gitlab.com/app=$CI_PROJECT_PATH_SLUG</code> + and + <code>app.gitlab.com/env=$CI_ENVIRONMENT_SLUG</code>. + </span> + </section> + </div> + </template> + </div> +</template> diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 347828888dc..1724cc692bd 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -226,7 +226,7 @@ export default { { deep: true }, ); const combinedActions = (manualActions || []).concat(scheduledActions || []); - return combinedActions.map(action => ({ + return combinedActions.map((action) => ({ ...action, name: action.name, })); diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue index 32528e6c6ea..48edde82ce7 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.vue +++ b/app/assets/javascripts/environments/components/environment_rollback.vue @@ -55,7 +55,7 @@ export default { retryUrl: this.retryUrl, isLastDeployment: this.isLastDeployment, }); - eventHub.$on('rollbackEnvironment', environment => { + eventHub.$on('rollbackEnvironment', (environment) => { if (environment.id === this.environment.id) { this.isLoading = true; } diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index b6a7cce36e9..6f68c6e864a 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -39,11 +39,6 @@ export default { type: String, required: true, }, - canaryDeploymentFeatureId: { - type: String, - required: false, - default: '', - }, canCreateEnvironment: { type: Boolean, required: true, @@ -75,11 +70,6 @@ export default { required: false, default: '', }, - showCanaryDeploymentCallout: { - type: Boolean, - required: false, - default: false, - }, userCalloutsPath: { type: String, required: false, @@ -116,7 +106,7 @@ export default { this.service .getFolderContent(folder.folder_path) - .then(response => this.store.setfolderContent(folder, response.data.environments)) + .then((response) => this.store.setfolderContent(folder, response.data.environments)) .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false)) .catch(() => { Flash(s__('Environments|An error occurred while fetching the environments.')); @@ -130,7 +120,7 @@ export default { // We need to verify if any folder is open to also update it const openFolders = this.store.getOpenFolders(); if (openFolders.length) { - openFolders.forEach(folder => this.fetchChildEnvironments(folder)); + openFolders.forEach((folder) => this.fetchChildEnvironments(folder)); } }, }, @@ -205,8 +195,6 @@ export default { :environments="state.environments" :pagination="state.paginationInformation" :can-read-environment="canReadEnvironment" - :canary-deployment-feature-id="canaryDeploymentFeatureId" - :show-canary-deployment-callout="showCanaryDeploymentCallout" :user-callouts-path="userCalloutsPath" :lock-promotion-svg-path="lockPromotionSvgPath" :help-canary-deployments-path="helpCanaryDeploymentsPath" diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index d13c7204285..bbb56ca6f26 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -6,16 +6,16 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { flow, reverse, sortBy } from 'lodash/fp'; import { s__ } from '~/locale'; import EnvironmentItem from './environment_item.vue'; +import DeployBoard from './deploy_board.vue'; +import CanaryUpdateModal from './canary_update_modal.vue'; export default { components: { EnvironmentItem, GlLoadingIcon, - DeployBoard: () => import('ee_component/environments/components/deploy_board_component.vue'), - CanaryDeploymentCallout: () => - import('ee_component/environments/components/canary_deployment_callout.vue'), + DeployBoard, EnvironmentAlert: () => import('ee_component/environments/components/environment_alert.vue'), - CanaryUpdateModal: () => import('ee_component/environments/components/canary_update_modal.vue'), + CanaryUpdateModal, }, props: { environments: { @@ -33,11 +33,6 @@ export default { required: false, default: false, }, - canaryDeploymentFeatureId: { - type: String, - required: false, - default: '', - }, helpCanaryDeploymentsPath: { type: String, required: false, @@ -48,11 +43,6 @@ export default { required: false, default: '', }, - showCanaryDeploymentCallout: { - type: Boolean, - required: false, - default: false, - }, userCalloutsPath: { type: String, required: false, @@ -67,7 +57,7 @@ export default { }, computed: { sortedEnvironments() { - return this.sortEnvironments(this.environments).map(env => + return this.sortEnvironments(this.environments).map((env) => this.shouldRenderFolderContent(env) ? { ...env, children: this.sortEnvironments(env.children) } : env, @@ -121,9 +111,6 @@ export default { shouldRenderFolderContent(env) { return env.isFolder && env.isOpen && env.children && env.children.length > 0; }, - shouldShowCanaryCallout(env) { - return env.showCanaryCallout && this.showCanaryDeploymentCallout; - }, shouldRenderAlert(env) { return env?.has_opened_alert; }, @@ -144,11 +131,11 @@ export default { * 5. Put folders first. */ return flow( - sortBy(env => (env.isFolder ? env.folderName : env.name)), + sortBy((env) => (env.isFolder ? env.folderName : env.name)), reverse, - sortBy(env => (env.last_deployment ? env.last_deployment.created_at : '0000')), + sortBy((env) => (env.last_deployment ? env.last_deployment.created_at : '0000')), reverse, - sortBy(env => (env.isFolder ? -1 : 1)), + sortBy((env) => (env.isFolder ? -1 : 1)), )(environments); }, changeCanaryWeight(model, weight) { @@ -241,17 +228,6 @@ export default { </div> </template> </template> - - <template v-if="shouldShowCanaryCallout(model)"> - <canary-deployment-callout - :key="`canary-promo-${i}`" - :canary-deployment-feature-id="canaryDeploymentFeatureId" - :user-callouts-path="userCalloutsPath" - :lock-promotion-svg-path="lockPromotionSvgPath" - :help-canary-deployments-path="helpCanaryDeploymentsPath" - :data-js-canary-promo-key="i" - /> - </template> </template> </div> </template> diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js new file mode 100644 index 00000000000..6d427bef4e6 --- /dev/null +++ b/app/assets/javascripts/environments/constants.js @@ -0,0 +1,40 @@ +import { __ } from '~/locale'; + +// These statuses are based on how the backend defines pod phases here +// lib/gitlab/kubernetes/pod.rb + +export const STATUS_MAP = { + succeeded: { + class: 'succeeded', + text: __('Succeeded'), + stable: true, + }, + running: { + class: 'running', + text: __('Running'), + stable: true, + }, + failed: { + class: 'failed', + text: __('Failed'), + stable: true, + }, + pending: { + class: 'pending', + text: __('Pending'), + stable: true, + }, + unknown: { + class: 'unknown', + text: __('Unknown'), + stable: true, + }, +}; + +export const CANARY_STATUS = { + class: 'canary-icon', + text: __('Canary'), + stable: false, +}; + +export const CANARY_UPDATE_MODAL = 'confirm-canary-change'; diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js index 6c547c3713a..e4726412f99 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js +++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js @@ -1,6 +1,5 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import canaryCalloutMixin from '../mixins/canary_callout_mixin'; import environmentsFolderApp from './environments_folder_view.vue'; import { parseBoolean } from '../../lib/utils/common_utils'; import Translate from '../../vue_shared/translate'; @@ -21,7 +20,6 @@ export default () => { components: { environmentsFolderApp, }, - mixins: [canaryCalloutMixin], apolloProvider, provide: { projectPath: el.dataset.projectPath, @@ -43,7 +41,6 @@ export default () => { folderName: this.folderName, cssContainerClass: this.cssContainerClass, canReadEnvironment: this.canReadEnvironment, - ...this.canaryCalloutProps, }, }); }, diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue index 25f5483c58b..dbb60fa4622 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.vue +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -34,16 +34,6 @@ export default { type: Boolean, required: true, }, - canaryDeploymentFeatureId: { - type: String, - required: false, - default: '', - }, - showCanaryDeploymentCallout: { - type: Boolean, - required: false, - default: false, - }, userCalloutsPath: { type: String, required: false, @@ -98,8 +88,6 @@ export default { :environments="state.environments" :pagination="state.paginationInformation" :can-read-environment="canReadEnvironment" - :canary-deployment-feature-id="canaryDeploymentFeatureId" - :show-canary-deployment-callout="showCanaryDeploymentCallout" :user-callouts-path="userCalloutsPath" :lock-promotion-svg-path="lockPromotionSvgPath" :help-canary-deployments-path="helpCanaryDeploymentsPath" diff --git a/app/assets/javascripts/environments/graphql/mutations/update_canary_ingress.mutation.graphql b/app/assets/javascripts/environments/graphql/mutations/update_canary_ingress.mutation.graphql new file mode 100644 index 00000000000..04ea5cbcaef --- /dev/null +++ b/app/assets/javascripts/environments/graphql/mutations/update_canary_ingress.mutation.graphql @@ -0,0 +1,5 @@ +mutation($input: EnvironmentsCanaryIngressUpdateInput!) { + environmentsCanaryIngressUpdate(input: $input) { + errors + } +} diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js index 8e8af3f32f7..4d734a457ab 100644 --- a/app/assets/javascripts/environments/index.js +++ b/app/assets/javascripts/environments/index.js @@ -1,6 +1,5 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; -import canaryCalloutMixin from './mixins/canary_callout_mixin'; import environmentsComponent from './components/environments_app.vue'; import { parseBoolean } from '../lib/utils/common_utils'; import Translate from '../vue_shared/translate'; @@ -20,7 +19,6 @@ export default () => { components: { environmentsComponent, }, - mixins: [canaryCalloutMixin], apolloProvider, provide: { projectPath: el.dataset.projectPath, @@ -46,7 +44,6 @@ export default () => { deployBoardsHelpPath: this.deployBoardsHelpPath, canCreateEnvironment: this.canCreateEnvironment, canReadEnvironment: this.canReadEnvironment, - ...this.canaryCalloutProps, }, }); }, diff --git a/app/assets/javascripts/environments/mixins/canary_callout_mixin.js b/app/assets/javascripts/environments/mixins/canary_callout_mixin.js deleted file mode 100644 index e9f1a144cb3..00000000000 --- a/app/assets/javascripts/environments/mixins/canary_callout_mixin.js +++ /dev/null @@ -1,26 +0,0 @@ -import { parseBoolean } from '~/lib/utils/common_utils'; - -export default { - data() { - const data = this.$options.el.dataset; - - return { - canaryDeploymentFeatureId: data.environmentsDataCanaryDeploymentFeatureId, - showCanaryDeploymentCallout: parseBoolean(data.environmentsDataShowCanaryDeploymentCallout), - userCalloutsPath: data.environmentsDataUserCalloutsPath, - lockPromotionSvgPath: data.environmentsDataLockPromotionSvgPath, - helpCanaryDeploymentsPath: data.environmentsDataHelpCanaryDeploymentsPath, - }; - }, - computed: { - canaryCalloutProps() { - return { - canaryDeploymentFeatureId: this.canaryDeploymentFeatureId, - showCanaryDeploymentCallout: this.showCanaryDeploymentCallout, - userCalloutsPath: this.userCalloutsPath, - lockPromotionSvgPath: this.lockPromotionSvgPath, - helpCanaryDeploymentsPath: this.helpCanaryDeploymentsPath, - }; - }, - }, -}; diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js index 9b0301bba07..15a00c11ee6 100644 --- a/app/assets/javascripts/environments/mixins/environments_mixin.js +++ b/app/assets/javascripts/environments/mixins/environments_mixin.js @@ -3,7 +3,7 @@ */ import { isEqual, isFunction, omitBy } from 'lodash'; import Visibility from 'visibilityjs'; -import EnvironmentsStore from 'ee_else_ce/environments/stores/environments_store'; +import EnvironmentsStore from '../stores/environments_store'; import Poll from '../../lib/utils/poll'; import { getParameterByName } from '../../lib/utils/common_utils'; import { s__ } from '../../locale'; @@ -64,7 +64,7 @@ export default { }, filterNilValues(obj) { - return omitBy(obj, value => value === undefined || value === null); + return omitBy(obj, (value) => value === undefined || value === null); }, /** @@ -80,7 +80,7 @@ export default { // fetch new data return this.service .fetchEnvironments(this.requestData) - .then(response => { + .then((response) => { this.successCallback(response); this.poll.enable({ data: this.requestData, response }); }) @@ -107,7 +107,7 @@ export default { this.service .postAction(endpoint) .then(() => this.fetchEnvironments()) - .catch(err => { + .catch((err) => { this.isLoading = false; Flash(isFunction(errorMessage) ? errorMessage(err.response.data) : errorMessage); }); @@ -219,7 +219,7 @@ export default { data: this.requestData, successCallback: this.successCallback, errorCallback: this.errorCallback, - notificationCallback: isMakingRequest => { + notificationCallback: (isMakingRequest) => { this.isMakingRequest = isMakingRequest; }, }); diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js index 1992e753255..8911885e920 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js +++ b/app/assets/javascripts/environments/stores/environments_store.js @@ -1,4 +1,4 @@ -import { setDeployBoard } from 'ee_else_ce/environments/stores/helpers'; +import { setDeployBoard } from './helpers'; import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; /** @@ -46,9 +46,9 @@ export default class EnvironmentsStore { * @returns {Array} */ storeEnvironments(environments = []) { - const filteredEnvironments = environments.map(env => { + const filteredEnvironments = environments.map((env) => { const oldEnvironmentState = - this.state.environments.find(element => { + this.state.environments.find((element) => { if (env.latest) { return element.id === env.latest.id; } @@ -135,12 +135,22 @@ export default class EnvironmentsStore { /** * Toggles deploy board visibility for the provided environment ID. - * Currently only works on EE. * * @param {Object} environment * @return {Array} */ - toggleDeployBoard() { + toggleDeployBoard(environmentID) { + const environments = this.state.environments.slice(); + + this.state.environments = environments.map((env) => { + let updated = { ...env }; + + if (env.id === environmentID) { + updated = { ...updated, isDeployBoardVisible: !env.isDeployBoardVisible }; + } + return updated; + }); + return this.state.environments; } @@ -163,7 +173,7 @@ export default class EnvironmentsStore { * @return {Object} */ setfolderContent(folder, environments) { - const updatedEnvironments = environments.map(env => { + const updatedEnvironments = environments.map((env) => { let updated = env; if (env.latest) { @@ -192,7 +202,7 @@ export default class EnvironmentsStore { updateEnvironmentProp(environment, prop, newValue) { const { environments } = this.state; - const updatedEnvironments = environments.map(env => { + const updatedEnvironments = environments.map((env) => { const updateEnv = { ...env }; if (env.id === environment.id) { updateEnv[prop] = newValue; @@ -207,6 +217,6 @@ export default class EnvironmentsStore { getOpenFolders() { const { environments } = this.state; - return environments.filter(env => env.isFolder && env.isOpen); + return environments.filter((env) => env.isFolder && env.isOpen); } } diff --git a/app/assets/javascripts/environments/stores/helpers.js b/app/assets/javascripts/environments/stores/helpers.js index eb47ba29412..89457da0614 100644 --- a/app/assets/javascripts/environments/stores/helpers.js +++ b/app/assets/javascripts/environments/stores/helpers.js @@ -1,7 +1,22 @@ /** - * Deploy boards are EE only. - * * @param {Object} environment * @returns {Object} */ -export const setDeployBoard = (oldEnvironmentState, environment) => environment; +export const setDeployBoard = (oldEnvironmentState, environment) => { + let parsedEnvironment = environment; + if (environment.size === 1 && environment.rollout_status) { + parsedEnvironment = { + ...environment, + hasDeployBoard: true, + isDeployBoardVisible: + oldEnvironmentState.isDeployBoardVisible === false + ? oldEnvironmentState.isDeployBoardVisible + : true, + deployBoardData: + environment.rollout_status.status === 'found' ? environment.rollout_status : {}, + isLoadingDeployBoard: environment.rollout_status.status === 'loading', + isEmptyDeployBoard: environment.rollout_status.status === 'not_found', + }; + } + return parsedEnvironment; +}; diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue index 8272260705b..e21c6b62b91 100644 --- a/app/assets/javascripts/error_tracking/components/error_details.vue +++ b/app/assets/javascripts/error_tracking/components/error_details.vue @@ -87,7 +87,7 @@ export default { }; }, pollInterval: 2000, - update: data => data.project.sentryErrors.detailedError, + update: (data) => data.project.sentryErrors.detailedError, error: () => createFlash(__('Failed to load error details from Sentry.')), result(res) { if (res.data.project?.sentryErrors?.detailedError) { @@ -213,7 +213,7 @@ export default { this.errorStatus === errorStatus.RESOLVED ? errorStatus.UNRESOLVED : errorStatus.RESOLVED; // eslint-disable-next-line promise/catch-or-return - this.updateResolveStatus({ endpoint: this.issueUpdatePath, status }).then(res => { + this.updateResolveStatus({ endpoint: this.issueUpdatePath, status }).then((res) => { this.closedIssueId = res.closed_issue_iid; if (this.closedIssueId) { this.isAlertVisible = true; diff --git a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue index c3471346a63..dd320676e98 100644 --- a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue +++ b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue @@ -100,7 +100,7 @@ export default { /> <gl-sprintf v-if="errorFn" :message="__('%{spanStart}in%{spanEnd} %{errorFn}')"> - <template #span="{content}"> + <template #span="{ content }"> <span class="gl-text-gray-200">{{ content }} </span> </template> <template #errorFn> @@ -109,7 +109,7 @@ export default { </gl-sprintf> <gl-sprintf :message="__('%{spanStart}at line%{spanEnd} %{errorLine}%{errorColumn}')"> - <template #span="{content}"> + <template #span="{ content }"> <span class="gl-text-gray-200">{{ content }} </span> </template> <template #errorLine> diff --git a/app/assets/javascripts/error_tracking/store/actions.js b/app/assets/javascripts/error_tracking/store/actions.js index b52405248d8..8f1e7e0b959 100644 --- a/app/assets/javascripts/error_tracking/store/actions.js +++ b/app/assets/javascripts/error_tracking/store/actions.js @@ -11,7 +11,7 @@ export const setStatus = ({ commit }, status) => { export const updateStatus = ({ commit }, { endpoint, redirectUrl, status }) => service .updateErrorStatus(endpoint, status) - .then(resp => { + .then((resp) => { commit(types.SET_ERROR_STATUS, status); if (redirectUrl) visitUrl(redirectUrl); diff --git a/app/assets/javascripts/error_tracking/store/details/actions.js b/app/assets/javascripts/error_tracking/store/details/actions.js index df5be5224a7..394dec938cf 100644 --- a/app/assets/javascripts/error_tracking/store/details/actions.js +++ b/app/assets/javascripts/error_tracking/store/details/actions.js @@ -6,7 +6,7 @@ import { __ } from '~/locale'; let stackTracePoll; -const stopPolling = poll => { +const stopPolling = (poll) => { if (poll) poll.stop(); }; diff --git a/app/assets/javascripts/error_tracking/store/details/getters.js b/app/assets/javascripts/error_tracking/store/details/getters.js index a3b31436c81..4e159b3931f 100644 --- a/app/assets/javascripts/error_tracking/store/details/getters.js +++ b/app/assets/javascripts/error_tracking/store/details/getters.js @@ -1,4 +1,4 @@ -export const stacktrace = state => +export const stacktrace = (state) => state.stacktraceData.stack_trace_entries ? state.stacktraceData.stack_trace_entries.reverse() : []; diff --git a/app/assets/javascripts/error_tracking/store/list/mutations.js b/app/assets/javascripts/error_tracking/store/list/mutations.js index be0cd4de78d..84a62fa9024 100644 --- a/app/assets/javascripts/error_tracking/store/list/mutations.js +++ b/app/assets/javascripts/error_tracking/store/list/mutations.js @@ -17,7 +17,7 @@ export default { return; } // remove any existing item, then add it to the start of the list - const recentSearches = state.recentSearches.filter(s => s !== searchTerm); + const recentSearches = state.recentSearches.filter((s) => s !== searchTerm); recentSearches.unshift(searchTerm); // only keep the last 5 state.recentSearches = recentSearches.slice(0, 5); @@ -60,7 +60,7 @@ export default { state.endpoint = endpoint; }, [types.REMOVE_IGNORED_RESOLVED_ERRORS](state, error) { - state.errors = state.errors.filter(err => err.id !== error); + state.errors = state.errors.filter((err) => err.id !== error); }, [types.SET_STATUS_FILTER](state, query) { state.statusFilter = query; diff --git a/app/assets/javascripts/error_tracking/utils.js b/app/assets/javascripts/error_tracking/utils.js index 5b705cc5510..aeed5450022 100644 --- a/app/assets/javascripts/error_tracking/utils.js +++ b/app/assets/javascripts/error_tracking/utils.js @@ -4,7 +4,7 @@ * Tracks snowplow event when User clicks on error link to Sentry * @param {String} externalUrl that will be send as a property for the event */ -export const trackClickErrorLinkToSentryOptions = url => ({ +export const trackClickErrorLinkToSentryOptions = (url) => ({ category: 'Error Tracking', action: 'click_error_link_to_sentry', label: 'Error Link', @@ -30,7 +30,7 @@ export const trackErrorDetailsViewsOptions = { /** * Tracks snowplow event when error status is updated */ -export const trackErrorStatusUpdateOptions = status => ({ +export const trackErrorStatusUpdateOptions = (status) => ({ category: 'Error Tracking', action: `update_${status}_status`, }); diff --git a/app/assets/javascripts/error_tracking_settings/store/actions.js b/app/assets/javascripts/error_tracking_settings/store/actions.js index 27433178c8e..2821798f82d 100644 --- a/app/assets/javascripts/error_tracking_settings/store/actions.js +++ b/app/assets/javascripts/error_tracking_settings/store/actions.js @@ -63,7 +63,7 @@ export const updateSettings = ({ dispatch, state }) => { .then(() => { refreshCurrentPage(); }) - .catch(err => { + .catch((err) => { dispatch('receiveSettingsError', err); }); }; diff --git a/app/assets/javascripts/error_tracking_settings/store/getters.js b/app/assets/javascripts/error_tracking_settings/store/getters.js index a02a4310ab9..30828778574 100644 --- a/app/assets/javascripts/error_tracking_settings/store/getters.js +++ b/app/assets/javascripts/error_tracking_settings/store/getters.js @@ -2,12 +2,12 @@ import { isMatch } from 'lodash'; import { __, s__, sprintf } from '~/locale'; import { getDisplayName } from '../utils'; -export const hasProjects = state => Boolean(state.projects) && state.projects.length > 0; +export const hasProjects = (state) => Boolean(state.projects) && state.projects.length > 0; export const isProjectInvalid = (state, getters) => Boolean(state.selectedProject) && getters.hasProjects && - !state.projects.some(project => isMatch(state.selectedProject, project)); + !state.projects.some((project) => isMatch(state.selectedProject, project)); export const dropdownLabel = (state, getters) => { if (state.selectedProject !== null) { @@ -19,7 +19,7 @@ export const dropdownLabel = (state, getters) => { return s__('ErrorTracking|Select project'); }; -export const invalidProjectLabel = state => { +export const invalidProjectLabel = (state) => { if (state.selectedProject) { return sprintf( __('Project "%{name}" is no longer available. Select another project to continue.'), @@ -31,7 +31,7 @@ export const invalidProjectLabel = state => { return ''; }; -export const projectSelectionLabel = state => { +export const projectSelectionLabel = (state) => { if (state.token) { return s__( "ErrorTracking|Click 'Connect' to re-establish the connection to Sentry and activate the dropdown.", diff --git a/app/assets/javascripts/error_tracking_settings/store/mutations.js b/app/assets/javascripts/error_tracking_settings/store/mutations.js index e1986eb694b..1fc028093c1 100644 --- a/app/assets/javascripts/error_tracking_settings/store/mutations.js +++ b/app/assets/javascripts/error_tracking_settings/store/mutations.js @@ -12,7 +12,7 @@ export default { .map(convertObjectPropsToCamelCase) // The `pick` strips out extra properties returned from Sentry. // Such properties could be problematic later, e.g. when checking whether `projects` contains `selectedProject` - .map(project => pick(project, projectKeys)); + .map((project) => pick(project, projectKeys)); }, [types.RESET_CONNECT](state) { state.connectSuccessful = false; diff --git a/app/assets/javascripts/error_tracking_settings/utils.js b/app/assets/javascripts/error_tracking_settings/utils.js index 9a09702a030..5d18ac8e802 100644 --- a/app/assets/javascripts/error_tracking_settings/utils.js +++ b/app/assets/javascripts/error_tracking_settings/utils.js @@ -13,4 +13,4 @@ export const transformFrontendSettings = ({ apiHost, enabled, token, selectedPro return { api_host: apiHost || null, enabled, token: token || null, project }; }; -export const getDisplayName = project => `${project.organizationName} | ${project.slug}`; +export const getDisplayName = (project) => `${project.organizationName} | ${project.slug}`; diff --git a/app/assets/javascripts/experimental_flags.js b/app/assets/javascripts/experimental_flags.js index 42b3fb8c6da..1d60847147b 100644 --- a/app/assets/javascripts/experimental_flags.js +++ b/app/assets/javascripts/experimental_flags.js @@ -2,7 +2,7 @@ import $ from 'jquery'; import Cookies from 'js-cookie'; export default () => { - $('.js-experiment-feature-toggle').on('change', e => { + $('.js-experiment-feature-toggle').on('change', (e) => { const el = e.target; Cookies.set(el.name, el.value, { diff --git a/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue b/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue index 5953a4fbad8..5fcca11e695 100644 --- a/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue +++ b/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue @@ -31,6 +31,13 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + inject: [ + 'projectName', + 'featureFlagsHelpPagePath', + 'unleashApiUrl', + 'featureFlagsClientExampleHelpPagePath', + 'featureFlagsClientLibrariesHelpPagePath', + ], props: { instanceId: { @@ -55,13 +62,6 @@ export default { required: true, }, }, - inject: [ - 'projectName', - 'featureFlagsHelpPagePath', - 'unleashApiUrl', - 'featureFlagsClientExampleHelpPagePath', - 'featureFlagsClientLibrariesHelpPagePath', - ], translations: { cancelActionLabel: __('Close'), modalTitle: s__('FeatureFlags|Configure feature flags'), diff --git a/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue b/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue index b89e9723606..210212fa900 100644 --- a/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue +++ b/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue @@ -30,9 +30,6 @@ export default { }; }, translations: { - legacyFlagAlert: s__( - 'FeatureFlags|GitLab is moving to a new way of managing feature flags, and in 13.4, this feature flag will become read-only. Please create a new feature flag.', - ), legacyReadOnlyFlagAlert: s__( 'FeatureFlags|GitLab is moving to a new way of managing feature flags. This feature flag is read-only, and it will be removed in 14.0. Please create a new feature flag.', ), @@ -59,18 +56,6 @@ export default { deprecated() { return this.version === LEGACY_FLAG; }, - deprecatedAndEditable() { - return this.deprecated && !this.hasLegacyReadOnlyFlags; - }, - deprecatedAndReadOnly() { - return this.deprecated && this.hasLegacyReadOnlyFlags; - }, - hasLegacyReadOnlyFlags() { - return ( - this.glFeatures.featureFlagsLegacyReadOnly && - !this.glFeatures.featureFlagsLegacyReadOnlyOverride - ); - }, }, created() { return this.fetchFeatureFlag(); @@ -91,12 +76,9 @@ export default { <gl-loading-icon v-if="isLoading" size="xl" class="gl-mt-7" /> <template v-else-if="!isLoading && !hasError"> - <gl-alert v-if="deprecatedAndEditable" variant="warning" :dismissible="false" class="gl-my-5"> - {{ $options.translations.legacyFlagAlert }} - </gl-alert> - <gl-alert v-if="deprecatedAndReadOnly" variant="warning" :dismissible="false" class="gl-my-5"> - {{ $options.translations.legacyReadOnlyFlagAlert }} - </gl-alert> + <gl-alert v-if="deprecated" variant="warning" :dismissible="false" class="gl-my-5">{{ + $options.translations.legacyReadOnlyFlagAlert + }}</gl-alert> <div class="gl-display-flex gl-align-items-center gl-mb-4 gl-mt-4"> <gl-toggle :value="active" @@ -122,7 +104,7 @@ export default { :submit-text="__('Save changes')" :active="active" :version="version" - @handleSubmit="data => updateFeatureFlag(data)" + @handleSubmit="(data) => updateFeatureFlag(data)" /> </template> </div> diff --git a/app/assets/javascripts/feature_flags/components/environments_dropdown.vue b/app/assets/javascripts/feature_flags/components/environments_dropdown.vue index 3caf536b6a2..88f1e692f5f 100644 --- a/app/assets/javascripts/feature_flags/components/environments_dropdown.vue +++ b/app/assets/javascripts/feature_flags/components/environments_dropdown.vue @@ -1,6 +1,6 @@ <script> import { debounce } from 'lodash'; -import { GlDeprecatedButton, GlSearchBoxByType } from '@gitlab/ui'; +import { GlButton, GlSearchBoxByType } from '@gitlab/ui'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; import { deprecatedCreateFlash as createFlash } from '~/flash'; @@ -28,9 +28,10 @@ import { deprecatedCreateFlash as createFlash } from '~/flash'; export default { name: 'EnvironmentsSearchableInput', components: { - GlDeprecatedButton, + GlButton, GlSearchBoxByType, }, + inject: ['environmentsEndpoint'], props: { value: { type: String, @@ -53,7 +54,6 @@ export default { required: false, }, }, - inject: ['environmentsEndpoint'], data() { return { environmentSearch: this.value, @@ -159,19 +159,20 @@ export default { <div class="dropdown-content"> <ul v-if="results.length"> <li v-for="(result, i) in results" :key="i"> - <gl-deprecated-button class="btn-transparent" @click="selectEnvironment(result)">{{ + <gl-button category="tertiary" @click="selectEnvironment(result)">{{ result - }}</gl-deprecated-button> + }}</gl-button> </li> </ul> <div v-else-if="!results.length" class="text-secondary gl-p-3"> {{ __('No matching results') }} </div> <div v-if="shouldRenderCreateButton" class="dropdown-footer"> - <gl-deprecated-button - class="js-create-button btn-blank dropdown-item" + <gl-button + category="tertiary" + class="js-create-button dropdown-item" @click="createClicked" - >{{ composedCreateButtonLabel }}</gl-deprecated-button + >{{ composedCreateButtonLabel }}</gl-button > </div> </div> diff --git a/app/assets/javascripts/feature_flags/components/feature_flags.vue b/app/assets/javascripts/feature_flags/components/feature_flags.vue index fe327a98605..ddeefd7b827 100644 --- a/app/assets/javascripts/feature_flags/components/feature_flags.vue +++ b/app/assets/javascripts/feature_flags/components/feature_flags.vue @@ -133,7 +133,7 @@ export default { }, updateFeatureFlagOptions(parameters) { const queryString = Object.keys(parameters) - .map(parameter => { + .map((parameter) => { const value = parameters[parameter]; return `${parameter}=${encodeURIComponent(value)}`; }) diff --git a/app/assets/javascripts/feature_flags/components/feature_flags_tab.vue b/app/assets/javascripts/feature_flags/components/feature_flags_tab.vue index 0539b5ff832..24b0b54d1be 100644 --- a/app/assets/javascripts/feature_flags/components/feature_flags_tab.vue +++ b/app/assets/javascripts/feature_flags/components/feature_flags_tab.vue @@ -3,6 +3,7 @@ import { GlAlert, GlBadge, GlEmptyState, GlLink, GlLoadingIcon, GlTab } from '@g export default { components: { GlAlert, GlBadge, GlEmptyState, GlLink, GlLoadingIcon, GlTab }, + inject: ['errorStateSvgPath', 'featureFlagsHelpPagePath'], props: { title: { required: true, @@ -46,7 +47,6 @@ export default { type: String, }, }, - inject: ['errorStateSvgPath', 'featureFlagsHelpPagePath'], computed: { itemCount() { return this.count ?? 0; diff --git a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue index ba46bab2df0..f3b199b5aca 100644 --- a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue +++ b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue @@ -17,13 +17,13 @@ export default { GlTooltip: GlTooltipDirective, }, mixins: [glFeatureFlagMixin()], + inject: ['csrfToken'], props: { featureFlags: { type: Array, required: true, }, }, - inject: ['csrfToken'], data() { return { deleteFeatureFlagUrl: null, @@ -31,19 +31,12 @@ export default { }; }, translations: { - legacyFlagAlert: s__('FeatureFlags|Flag becomes read only soon'), legacyFlagReadOnlyAlert: s__('FeatureFlags|Flag is read-only'), }, computed: { permissions() { return this.glFeatures.featureFlagPermissions; }, - isLegacyReadOnlyFlagsEnabled() { - return ( - this.glFeatures.featureFlagsLegacyReadOnly && - !this.glFeatures.featureFlagsLegacyReadOnlyOverride - ); - }, modalTitle() { return sprintf(s__('FeatureFlags|Delete %{name}?'), { name: this.deleteFeatureFlagName, @@ -57,18 +50,13 @@ export default { modalId() { return 'delete-feature-flag'; }, - legacyFlagToolTipText() { - const { legacyFlagReadOnlyAlert, legacyFlagAlert } = this.$options.translations; - - return this.isLegacyReadOnlyFlagsEnabled ? legacyFlagReadOnlyAlert : legacyFlagAlert; - }, }, methods: { isLegacyFlag(flag) { return flag.version !== NEW_VERSION_FLAG; }, statusToggleDisabled(flag) { - return this.isLegacyReadOnlyFlagsEnabled && flag.version === LEGACY_FLAG; + return flag.version === LEGACY_FLAG; }, scopeTooltipText(scope) { return !scope.active @@ -100,7 +88,7 @@ export default { return featureFlag.iid ? `^${featureFlag.iid}` : ''; }, canDeleteFlag(flag) { - return !this.permissions || (flag.scopes || []).every(scope => scope.can_update); + return !this.permissions || (flag.scopes || []).every((scope) => scope.can_update); }, setDeleteModalData(featureFlag) { this.deleteFeatureFlagUrl = featureFlag.destroy_path; @@ -123,9 +111,7 @@ export default { <template> <div class="table-holder js-feature-flag-table"> <div class="gl-responsive-table-row table-row-header" role="row"> - <div class="table-section section-10"> - {{ s__('FeatureFlags|ID') }} - </div> + <div class="table-section section-10">{{ s__('FeatureFlags|ID') }}</div> <div class="table-section section-10" role="columnheader"> {{ s__('FeatureFlags|Status') }} </div> @@ -161,9 +147,8 @@ export default { v-else-if="featureFlag.active" variant="success" data-testid="feature-flag-status-badge" + >{{ s__('FeatureFlags|Active') }}</gl-badge > - {{ s__('FeatureFlags|Active') }} - </gl-badge> <gl-badge v-else variant="danger">{{ s__('FeatureFlags|Inactive') }}</gl-badge> </div> </div> @@ -179,7 +164,7 @@ export default { </div> <gl-icon v-if="isLegacyFlag(featureFlag)" - v-gl-tooltip.hover="legacyFlagToolTipText" + v-gl-tooltip.hover="$options.translations.legacyFlagReadOnlyAlert" class="gl-ml-3" name="information-o" /> @@ -205,9 +190,8 @@ export default { :variant="badgeVariant(scope)" :data-qa-selector="`feature-flag-scope-${badgeVariant(scope)}-badge`" class="gl-mr-3 gl-mt-2" + >{{ badgeText(scope) }}</gl-badge > - {{ badgeText(scope) }} - </gl-badge> </template> <template v-else> <gl-badge @@ -216,9 +200,8 @@ export default { data-testid="strategy-badge" variant="info" class="gl-mr-3 gl-mt-2" + >{{ strategyBadgeText(strategy) }}</gl-badge > - {{ strategyBadgeText(strategy) }} - </gl-badge> </template> </div> </div> diff --git a/app/assets/javascripts/feature_flags/components/form.vue b/app/assets/javascripts/feature_flags/components/form.vue index 12856b79f63..253661ece1f 100644 --- a/app/assets/javascripts/feature_flags/components/form.vue +++ b/app/assets/javascripts/feature_flags/components/form.vue @@ -46,6 +46,11 @@ export default { GlTooltip: GlTooltipDirective, }, mixins: [featureFlagsMixin()], + inject: { + featureFlagIssuesEndpoint: { + default: '', + }, + }, props: { active: { type: Boolean, @@ -86,11 +91,6 @@ export default { default: LEGACY_FLAG, }, }, - inject: { - featureFlagIssuesEndpoint: { - default: '', - }, - }, translations: { allEnvironmentsText: s__('FeatureFlags|* (All Environments)'), @@ -117,7 +117,7 @@ export default { formDescription: this.description, // operate on a clone to avoid mutating props - formScopes: this.scopes.map(s => ({ ...s })), + formScopes: this.scopes.map((s) => ({ ...s })), formStrategies: cloneDeep(this.strategies), newScope: '', @@ -125,13 +125,13 @@ export default { }, computed: { filteredScopes() { - return this.formScopes.filter(scope => !scope.shouldBeDestroyed); + return this.formScopes.filter((scope) => !scope.shouldBeDestroyed); }, filteredStrategies() { - return this.formStrategies.filter(s => !s.shouldBeDestroyed); + return this.formStrategies.filter((s) => !s.shouldBeDestroyed); }, canUpdateFlag() { - return !this.permissionsFlag || (this.formScopes || []).every(scope => scope.canUpdate); + return !this.permissionsFlag || (this.formScopes || []).every((scope) => scope.canUpdate); }, permissionsFlag() { return this.glFeatures.featureFlagPermissions; @@ -143,11 +143,7 @@ export default { return this.featureFlagIssuesEndpoint.length > 0; }, readOnly() { - return ( - this.glFeatures.featureFlagsLegacyReadOnly && - !this.glFeatures.featureFlagsLegacyReadOnlyOverride && - this.version === LEGACY_FLAG - ); + return this.version === LEGACY_FLAG; }, }, methods: { @@ -167,7 +163,7 @@ export default { if (isNumber(s.id)) { Vue.set(s, 'shouldBeDestroyed', true); } else { - this.formStrategies = this.formStrategies.filter(strategy => strategy !== s); + this.formStrategies = this.formStrategies.filter((strategy) => strategy !== s); } }, @@ -188,7 +184,7 @@ export default { */ removeScope(scope) { if (isString(scope.id) && scope.id.startsWith(INTERNAL_ID_PREFIX)) { - this.formScopes = this.formScopes.filter(s => s !== scope); + this.formScopes = this.formScopes.filter((s) => s !== scope); } else { Vue.set(scope, 'shouldBeDestroyed', true); } @@ -387,9 +383,9 @@ export default { class="col-12" :value="scope.environmentScope" :disabled="!canUpdateScope(scope) || scope.environmentScope !== ''" - @selectEnvironment="env => (scope.environmentScope = env)" - @createClicked="env => (scope.environmentScope = env)" - @clearInput="env => (scope.environmentScope = '')" + @selectEnvironment="(env) => (scope.environmentScope = env)" + @createClicked="(env) => (scope.environmentScope = env)" + @clearInput="(env) => (scope.environmentScope = '')" /> <gl-badge v-if="permissionsFlag && scope.protected" variant="success"> @@ -406,7 +402,7 @@ export default { <toggle-button :value="scope.active" :disabled-input="!active || !canUpdateScope(scope)" - @change="status => (scope.active = status)" + @change="(status) => (scope.active = status)" /> </div> </div> @@ -524,8 +520,8 @@ export default { <environments-dropdown class="js-new-scope-name col-12" :value="newScope" - @selectEnvironment="env => createNewScope({ environmentScope: env })" - @createClicked="env => createNewScope({ environmentScope: env })" + @selectEnvironment="(env) => createNewScope({ environmentScope: env })" + @createClicked="(env) => createNewScope({ environmentScope: env })" /> </div> </div> diff --git a/app/assets/javascripts/feature_flags/components/new_feature_flag.vue b/app/assets/javascripts/feature_flags/components/new_feature_flag.vue index e6949d8028b..529fefd7e45 100644 --- a/app/assets/javascripts/feature_flags/components/new_feature_flag.vue +++ b/app/assets/javascripts/feature_flags/components/new_feature_flag.vue @@ -71,7 +71,7 @@ export default { :scopes="scopes" :strategies="strategies" :version="version" - @handleSubmit="data => createFeatureFlag(data)" + @handleSubmit="(data) => createFeatureFlag(data)" /> </div> </template> diff --git a/app/assets/javascripts/feature_flags/components/strategy.vue b/app/assets/javascripts/feature_flags/components/strategy.vue index ce03248381c..9593bcf6487 100644 --- a/app/assets/javascripts/feature_flags/components/strategy.vue +++ b/app/assets/javascripts/feature_flags/components/strategy.vue @@ -83,7 +83,7 @@ export default { ); }, filteredEnvironments() { - return this.environments.filter(e => !e.shouldBeDestroyed); + return this.environments.filter((e) => !e.shouldBeDestroyed); }, isPercentUserRollout() { return this.formStrategy.name === ROLLOUT_STRATEGY_PERCENT_ROLLOUT; @@ -91,7 +91,9 @@ export default { }, methods: { addEnvironment(environment) { - const allEnvironmentsScope = this.environments.find(scope => scope.environmentScope === '*'); + const allEnvironmentsScope = this.environments.find( + (scope) => scope.environmentScope === '*', + ); if (allEnvironmentsScope) { allEnvironmentsScope.shouldBeDestroyed = true; } @@ -113,7 +115,7 @@ export default { if (isNumber(environment.id)) { Vue.set(environment, 'shouldBeDestroyed', true); } else { - this.environments = this.environments.filter(e => e !== environment); + this.environments = this.environments.filter((e) => e !== environment); } if (this.filteredEnvironments.length === 0) { this.environments.push({ environmentScope: '*' }); diff --git a/app/assets/javascripts/feature_flags/components/strategy_parameters.vue b/app/assets/javascripts/feature_flags/components/strategy_parameters.vue index b6e06880315..a22f081bb92 100644 --- a/app/assets/javascripts/feature_flags/components/strategy_parameters.vue +++ b/app/assets/javascripts/feature_flags/components/strategy_parameters.vue @@ -30,7 +30,7 @@ export default { }, computed: { strategyComponent() { - return STRATEGIES[(this.strategy?.name)]; + return STRATEGIES[this.strategy?.name]; }, }, methods: { diff --git a/app/assets/javascripts/feature_flags/store/edit/actions.js b/app/assets/javascripts/feature_flags/store/edit/actions.js index 3678c2f7788..c4515e07a00 100644 --- a/app/assets/javascripts/feature_flags/store/edit/actions.js +++ b/app/assets/javascripts/feature_flags/store/edit/actions.js @@ -29,7 +29,7 @@ export const updateFeatureFlag = ({ state, dispatch }, params) => { dispatch('receiveUpdateFeatureFlagSuccess'); visitUrl(state.path); }) - .catch(error => dispatch('receiveUpdateFeatureFlagError', error.response.data)); + .catch((error) => dispatch('receiveUpdateFeatureFlagError', error.response.data)); }; export const requestUpdateFeatureFlag = ({ commit }) => commit(types.REQUEST_UPDATE_FEATURE_FLAG); diff --git a/app/assets/javascripts/feature_flags/store/edit/index.js b/app/assets/javascripts/feature_flags/store/edit/index.js index 81edc791924..65ea61c3025 100644 --- a/app/assets/javascripts/feature_flags/store/edit/index.js +++ b/app/assets/javascripts/feature_flags/store/edit/index.js @@ -4,7 +4,7 @@ import state from './state'; import * as actions from './actions'; import mutations from './mutations'; -export default data => +export default (data) => new Vuex.Store({ actions, mutations, diff --git a/app/assets/javascripts/feature_flags/store/gitlab_user_list/actions.js b/app/assets/javascripts/feature_flags/store/gitlab_user_list/actions.js index d4587713fed..a834524df6c 100644 --- a/app/assets/javascripts/feature_flags/store/gitlab_user_list/actions.js +++ b/app/assets/javascripts/feature_flags/store/gitlab_user_list/actions.js @@ -1,14 +1,14 @@ import Api from '~/api'; import * as types from './mutation_types'; -const getErrorMessages = error => [].concat(error?.response?.data?.message ?? error.message); +const getErrorMessages = (error) => [].concat(error?.response?.data?.message ?? error.message); export const fetchUserLists = ({ commit, state: { filter, projectId } }) => { commit(types.FETCH_USER_LISTS); return Api.searchFeatureFlagUserLists(projectId, filter) .then(({ data }) => commit(types.RECEIVE_USER_LISTS_SUCCESS, data)) - .catch(error => commit(types.RECEIVE_USER_LISTS_ERROR, getErrorMessages(error))); + .catch((error) => commit(types.RECEIVE_USER_LISTS_ERROR, getErrorMessages(error))); }; export const setFilter = ({ commit, dispatch }, filter) => { diff --git a/app/assets/javascripts/feature_flags/store/gitlab_user_list/index.js b/app/assets/javascripts/feature_flags/store/gitlab_user_list/index.js index d25b574981f..5f2726770d5 100644 --- a/app/assets/javascripts/feature_flags/store/gitlab_user_list/index.js +++ b/app/assets/javascripts/feature_flags/store/gitlab_user_list/index.js @@ -3,7 +3,7 @@ import mutations from './mutations'; import * as actions from './actions'; import * as getters from './getters'; -export default data => ({ +export default (data) => ({ state: state(data), actions, getters, diff --git a/app/assets/javascripts/feature_flags/store/helpers.js b/app/assets/javascripts/feature_flags/store/helpers.js index d42e5c504db..2fa20e25f4e 100644 --- a/app/assets/javascripts/feature_flags/store/helpers.js +++ b/app/assets/javascripts/feature_flags/store/helpers.js @@ -17,16 +17,16 @@ import { * objects that is easier/nicer to bind to in Vue. * @param {Array} scopesFromRails An array of scope objects fetched from the API */ -export const mapToScopesViewModel = scopesFromRails => - (scopesFromRails || []).map(s => { +export const mapToScopesViewModel = (scopesFromRails) => + (scopesFromRails || []).map((s) => { const percentStrategy = (s.strategies || []).find( - strat => strat.name === ROLLOUT_STRATEGY_PERCENT_ROLLOUT, + (strat) => strat.name === ROLLOUT_STRATEGY_PERCENT_ROLLOUT, ); const rolloutPercentage = fetchPercentageParams(percentStrategy) || DEFAULT_PERCENT_ROLLOUT; const userStrategy = (s.strategies || []).find( - strat => strat.name === ROLLOUT_STRATEGY_USER_ID, + (strat) => strat.name === ROLLOUT_STRATEGY_USER_ID, ); const rolloutStrategy = @@ -36,7 +36,7 @@ export const mapToScopesViewModel = scopesFromRails => const rolloutUserIds = (fetchUserIdParams(userStrategy) || '') .split(',') - .filter(id => id) + .filter((id) => id) .join(', '); return { @@ -59,8 +59,8 @@ export const mapToScopesViewModel = scopesFromRails => * the shape that the Rails API expects. * @param {Array} scopesFromVue An array of scope objects from the Vue component */ -export const mapFromScopesViewModel = params => { - const scopes = (params.scopes || []).map(s => { +export const mapFromScopesViewModel = (params) => { + const scopes = (params.scopes || []).map((s) => { const parameters = {}; if (s.rolloutStrategy === ROLLOUT_STRATEGY_PERCENT_ROLLOUT) { parameters.groupId = PERCENT_ROLLOUT_GROUP_ID; @@ -145,32 +145,32 @@ export const createNewEnvironmentScope = (overrides = {}, featureFlagPermissions return newScope; }; -const mapStrategyScopesToRails = scopes => +const mapStrategyScopesToRails = (scopes) => scopes.length === 0 ? [{ environment_scope: '*' }] - : scopes.map(s => ({ + : scopes.map((s) => ({ id: s.id, _destroy: s.shouldBeDestroyed, environment_scope: s.environmentScope, })); -const mapStrategyScopesToView = scopes => - scopes.map(s => ({ +const mapStrategyScopesToView = (scopes) => + scopes.map((s) => ({ id: s.id, // eslint-disable-next-line no-underscore-dangle shouldBeDestroyed: Boolean(s._destroy), environmentScope: s.environment_scope, })); -const mapStrategiesParametersToViewModel = params => { +const mapStrategiesParametersToViewModel = (params) => { if (params.userIds) { return { ...params, userIds: params.userIds.split(',').join(', ') }; } return params; }; -export const mapStrategiesToViewModel = strategiesFromRails => - (strategiesFromRails || []).map(s => ({ +export const mapStrategiesToViewModel = (strategiesFromRails) => + (strategiesFromRails || []).map((s) => ({ id: s.id, name: s.name, parameters: mapStrategiesParametersToViewModel(s.parameters), @@ -180,14 +180,14 @@ export const mapStrategiesToViewModel = strategiesFromRails => scopes: mapStrategyScopesToView(s.scopes), })); -const mapStrategiesParametersToRails = params => { +const mapStrategiesParametersToRails = (params) => { if (params.userIds) { return { ...params, userIds: params.userIds.replace(/\s*,\s*/g, ',') }; } return params; }; -const mapStrategyToRails = strategy => { +const mapStrategyToRails = (strategy) => { const mappedStrategy = { id: strategy.id, name: strategy.name, @@ -202,7 +202,7 @@ const mapStrategyToRails = strategy => { return mappedStrategy; }; -export const mapStrategiesToRails = params => ({ +export const mapStrategiesToRails = (params) => ({ operations_feature_flag: { name: params.name, description: params.description, diff --git a/app/assets/javascripts/feature_flags/store/index/actions.js b/app/assets/javascripts/feature_flags/store/index/actions.js index a8c1a72c016..6b6b3d55e16 100644 --- a/app/assets/javascripts/feature_flags/store/index/actions.js +++ b/app/assets/javascripts/feature_flags/store/index/actions.js @@ -12,7 +12,7 @@ export const fetchFeatureFlags = ({ state, dispatch }) => { .get(state.endpoint, { params: state.options, }) - .then(response => + .then((response) => dispatch('receiveFeatureFlagsSuccess', { data: response.data || {}, headers: response.headers, @@ -46,7 +46,7 @@ export const toggleFeatureFlag = ({ dispatch }, flag) => { .put(flag.update_path, { operations_feature_flag: flag, }) - .then(response => dispatch('receiveUpdateFeatureFlagSuccess', response.data)) + .then((response) => dispatch('receiveUpdateFeatureFlagSuccess', response.data)) .catch(() => dispatch('receiveUpdateFeatureFlagError', flag.id)); }; @@ -62,7 +62,7 @@ export const deleteUserList = ({ state, dispatch }, list) => { return Api.deleteFeatureFlagUserList(state.projectId, list.iid) .then(() => dispatch('fetchUserLists')) - .catch(error => + .catch((error) => dispatch('receiveDeleteUserListError', { list, error: error?.response?.data ?? error, diff --git a/app/assets/javascripts/feature_flags/store/index/index.js b/app/assets/javascripts/feature_flags/store/index/index.js index f737e0517fc..76495a33232 100644 --- a/app/assets/javascripts/feature_flags/store/index/index.js +++ b/app/assets/javascripts/feature_flags/store/index/index.js @@ -3,7 +3,7 @@ import state from './state'; import * as actions from './actions'; import mutations from './mutations'; -export default data => +export default (data) => new Vuex.Store({ actions, mutations, diff --git a/app/assets/javascripts/feature_flags/store/index/mutations.js b/app/assets/javascripts/feature_flags/store/index/mutations.js index bdc23e66214..910b2ec42d4 100644 --- a/app/assets/javascripts/feature_flags/store/index/mutations.js +++ b/app/assets/javascripts/feature_flags/store/index/mutations.js @@ -4,7 +4,7 @@ import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../../constants'; import { mapToScopesViewModel } from '../helpers'; -const mapFlag = flag => ({ ...flag, scopes: mapToScopesViewModel(flag.scopes || []) }); +const mapFlag = (flag) => ({ ...flag, scopes: mapToScopesViewModel(flag.scopes || []) }); const updateFlag = (state, flag) => { const index = state[FEATURE_FLAG_SCOPE].findIndex(({ id }) => id === flag.id); @@ -74,12 +74,7 @@ export default { state.isRotating = true; state.hasRotateError = false; }, - [types.RECEIVE_ROTATE_INSTANCE_ID_SUCCESS]( - state, - { - data: { token }, - }, - ) { + [types.RECEIVE_ROTATE_INSTANCE_ID_SUCCESS](state, { data: { token } }) { state.isRotating = false; state.instanceId = token; state.hasRotateError = false; @@ -99,7 +94,7 @@ export default { updateFlag(state, { ...flag, active: !flag.active }); }, [types.REQUEST_DELETE_USER_LIST](state, list) { - state.userLists = state.userLists.filter(l => l !== list); + state.userLists = state.userLists.filter((l) => l !== list); }, [types.RECEIVE_DELETE_USER_LIST_ERROR](state, { error, list }) { state.isLoading = false; diff --git a/app/assets/javascripts/feature_flags/store/new/actions.js b/app/assets/javascripts/feature_flags/store/new/actions.js index e21c128cd39..6d595603819 100644 --- a/app/assets/javascripts/feature_flags/store/new/actions.js +++ b/app/assets/javascripts/feature_flags/store/new/actions.js @@ -27,7 +27,7 @@ export const createFeatureFlag = ({ state, dispatch }, params) => { dispatch('receiveCreateFeatureFlagSuccess'); visitUrl(state.path); }) - .catch(error => dispatch('receiveCreateFeatureFlagError', error.response.data)); + .catch((error) => dispatch('receiveCreateFeatureFlagError', error.response.data)); }; export const requestCreateFeatureFlag = ({ commit }) => commit(types.REQUEST_CREATE_FEATURE_FLAG); diff --git a/app/assets/javascripts/feature_flags/store/new/index.js b/app/assets/javascripts/feature_flags/store/new/index.js index 81edc791924..65ea61c3025 100644 --- a/app/assets/javascripts/feature_flags/store/new/index.js +++ b/app/assets/javascripts/feature_flags/store/new/index.js @@ -4,7 +4,7 @@ import state from './state'; import * as actions from './actions'; import mutations from './mutations'; -export default data => +export default (data) => new Vuex.Store({ actions, mutations, diff --git a/app/assets/javascripts/feature_flags/utils.js b/app/assets/javascripts/feature_flags/utils.js index 24c570657e6..e77cb8406cc 100644 --- a/app/assets/javascripts/feature_flags/utils.js +++ b/app/assets/javascripts/feature_flags/utils.js @@ -48,7 +48,7 @@ const badgeTextByType = { const scopeName = ({ environment_scope: scope }) => scope === ALL_ENVIRONMENTS_NAME ? s__('FeatureFlags|All Environments') : scope; -export const labelForStrategy = strategy => { +export const labelForStrategy = (strategy) => { const { name, parameters } = badgeTextByType[strategy.name]; if (parameters) { diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js index be55e6923c6..2da9aadd2b1 100644 --- a/app/assets/javascripts/feature_highlight/feature_highlight.js +++ b/app/assets/javascripts/feature_highlight/feature_highlight.js @@ -31,7 +31,7 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) { .removeAttr('disabled'); } -const getPriority = e => parseInt(e.dataset.highlightPriority, 10) || 0; +const getPriority = (e) => parseInt(e.dataset.highlightPriority, 10) || 0; export function findHighestPriorityFeature() { let priorityFeature; diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js index cfadfb26db2..fabc905d756 100644 --- a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js +++ b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js @@ -5,7 +5,7 @@ import { deprecatedCreateFlash as Flash } from '../flash'; import LazyLoader from '../lazy_loader'; import { togglePopover } from '../shared/popover'; -export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`; +export const getSelector = (highlightId) => `.js-feature-highlight[data-highlight=${highlightId}]`; export function dismiss(highlightId) { axios diff --git a/app/assets/javascripts/file_pickers.js b/app/assets/javascripts/file_pickers.js index 956a4954afb..fa5a5f14adb 100644 --- a/app/assets/javascripts/file_pickers.js +++ b/app/assets/javascripts/file_pickers.js @@ -1,7 +1,7 @@ export default function initFilePickers() { const filePickers = document.querySelectorAll('.js-filepicker'); - filePickers.forEach(filePicker => { + filePickers.forEach((filePicker) => { const button = filePicker.querySelector('.js-filepicker-button'); button.addEventListener('click', () => { diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index aad5647c045..0d7a475eb8e 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -33,8 +33,8 @@ export default { if (this.userCanCreateNote) { $diffFile - .on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e)) - .on('mouseleave', LINE_COLUMN_CLASSES, e => this.hideButton(this.isParallelView, e)); + .on('mouseover', LINE_COLUMN_CLASSES, (e) => this.showButton(this.isParallelView, e)) + .on('mouseleave', LINE_COLUMN_CLASSES, (e) => this.hideButton(this.isParallelView, e)); } }, diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index eabf3b0846e..a8670caf5b2 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -72,7 +72,7 @@ export default class FilterableList { .get(this.getFilterEndpoint(), { params, }) - .then(res => { + .then((res) => { this.onFilterSuccess(res, params); this.onFilterComplete(); }) diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js index 77491d1556b..588bd534224 100644 --- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js +++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js @@ -50,7 +50,7 @@ export default class AvailableDropdownMappings { }, }; - supportedTokens.forEach(type => { + supportedTokens.forEach((type) => { if (availableMappings[type]) { allowedMappings[type] = availableMappings[type]; } @@ -99,7 +99,7 @@ export default class AvailableDropdownMappings { // The DropdownNonUser class is hardcoded to look for and display a // "title" property, so we need to add this property to each release object - preprocessing: releases => releases.map(r => ({ ...r, title: r.tag })), + preprocessing: (releases) => releases.map((r) => ({ ...r, title: r.tag })), }, element: this.container.querySelector('#js-dropdown-release'), }, @@ -162,7 +162,7 @@ export default class AvailableDropdownMappings { extraArguments: { endpoint: this.getEnvironmentsEndpoint(), symbol: '', - preprocessing: data => data.map(env => ({ title: env })), + preprocessing: (data) => data.map((env) => ({ title: env })), }, element: this.container.querySelector('#js-dropdown-environment'), }, @@ -200,8 +200,9 @@ export default class AvailableDropdownMappings { } getMergeRequestTargetBranchesEndpoint() { - const endpoint = `${gon.relative_url_root || - ''}/autocomplete/merge_request_target_branches.json`; + const endpoint = `${ + gon.relative_url_root || '' + }/autocomplete/merge_request_target_branches.json`; const params = { group_id: this.getGroupId(), diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue index 0c4abc14494..4c2f55fd174 100644 --- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue +++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue @@ -28,16 +28,16 @@ export default { * fix by ignoring non-string recents while in Epic page. */ compatibleItems() { - return this.items.filter(item => typeof item === 'string'); + return this.items.filter((item) => typeof item === 'string'); }, processedItems() { - return this.compatibleItems.map(item => { + return this.compatibleItems.map((item) => { const { tokens, searchToken } = FilteredSearchTokenizer.processTokens( item, this.allowedKeys, ); - const resultantTokens = tokens.map(token => ({ + const resultantTokens = tokens.map((token) => ({ prefix: `${token.key}:`, operator: token.operator, suffix: `${token.symbol}${token.value}`, diff --git a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js index 30f412e590f..2c0c3024d38 100644 --- a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js +++ b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js @@ -33,7 +33,7 @@ export default class DropdownAjaxFilter extends FilteredSearchDropdown { } itemClicked(e) { - super.itemClicked(e, selected => { + super.itemClicked(e, (selected) => { const title = selected.querySelector('.dropdown-light-content').innerText.trim(); return DropdownUtils.getEscapedText(title); diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js index 1e3679b9e3c..001030b5f5f 100644 --- a/app/assets/javascripts/filtered_search/dropdown_emoji.js +++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js @@ -53,7 +53,7 @@ export default class DropdownEmoji extends FilteredSearchDropdown { } itemClicked(e) { - super.itemClicked(e, selected => { + super.itemClicked(e, (selected) => { const name = selected.querySelector('.js-data-value').innerText.trim(); return DropdownUtils.getEscapedText(name); }); @@ -69,7 +69,7 @@ export default class DropdownEmoji extends FilteredSearchDropdown { // Replace empty gl-emoji tag to real content const dropdownItems = [...this.dropdown.querySelectorAll('.filter-dropdown-item')]; - dropdownItems.forEach(dropdownItem => { + dropdownItems.forEach((dropdownItem) => { const valueElement = dropdownItem.querySelector('.js-data-value'); if (valueElement !== null) { const name = valueElement.innerText; diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js index 4f10b6ba9c3..1180f8683a1 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -83,7 +83,7 @@ export default class DropdownHint extends FilteredSearchDropdown { const dropdownData = this.tokenKeys .get() - .map(tokenKey => ({ + .map((tokenKey) => ({ icon: `${gon.sprite_icons}#${tokenKey.icon}`, hint: tokenKey.key, tag: `:${tokenKey.tag}`, diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js index bfa9f4a57ca..11261debeda 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js @@ -30,7 +30,7 @@ export default class DropdownNonUser extends FilteredSearchDropdown { } itemClicked(e) { - super.itemClicked(e, selected => { + super.itemClicked(e, (selected) => { const title = selected.querySelector('.js-data-value').innerText.trim(); return `${this.symbol}${DropdownUtils.getEscapedText(title)}`; }); diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index 43de86b09ee..22c98f360ed 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -61,7 +61,7 @@ export default class DropdownUtils { const { lastToken, tokens } = FilteredSearchTokenizer.processTokens(searchInput, allowedKeys); const lastKey = lastToken.key || lastToken || ''; const allowMultiple = item.type === 'array'; - const itemInExistingTokens = tokens.some(t => t.key === item.hint); + const itemInExistingTokens = tokens.some((t) => t.key === item.hint); const isSearchItem = updatedItem.hint === 'search'; if (isSearchItem) { @@ -77,7 +77,7 @@ export default class DropdownUtils { const tokenName = last(split[0].split(' ')); const match = isSearchItem - ? allowedKeys.some(key => key.startsWith(tokenName.toLowerCase())) + ? allowedKeys.some((key) => key.startsWith(tokenName.toLowerCase())) : updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1; updatedItem.droplab_hidden = tokenName ? match : false; @@ -129,12 +129,12 @@ export default class DropdownUtils { const values = []; if (untilInput) { - const inputIndex = tokens.findIndex(t => t.classList.contains('input-token')); + const inputIndex = tokens.findIndex((t) => t.classList.contains('input-token')); // Add one to include input-token to the tokens array tokens.splice(inputIndex + 1); } - tokens.forEach(token => { + tokens.forEach((token) => { if (token.classList.contains('js-visual-token')) { const name = token.querySelector('.name'); const operatorContainer = token.querySelector('.operator'); @@ -176,7 +176,7 @@ export default class DropdownUtils { } }); - return values.map(value => value.trim()).join(' '); + return values.map((value) => value.trim()).join(' '); } static getSearchInput(filteredSearchInput) { @@ -192,7 +192,7 @@ export default class DropdownUtils { // Replace all spaces inside quote marks with underscores // (will continue to match entire string until an end quote is found if any) // This helps with matching the beginning & end of a token:key - inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => + inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, (str) => str.replace(/\s/g, '_'), ); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js index 8626e1a3d18..7434cc4c5d1 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js @@ -28,7 +28,7 @@ export default class FilteredSearchDropdown { } getCurrentHook() { - return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null; + return this.droplab.hooks.filter((h) => h.id === this.hookId)[0] || null; } itemClicked(e, getValueFunction) { @@ -134,7 +134,7 @@ export default class FilteredSearchDropdown { if (!data) return; - const results = data.map(o => { + const results = data.map((o) => { const updated = o; updated.droplab_hidden = false; return updated; @@ -157,7 +157,7 @@ export default class FilteredSearchDropdown { // Iterate over all the static dropdown values, // then hide `None` and `Any` items. - Array.from(dropdownEl.querySelectorAll('li[data-value]')).forEach(itemEl => { + Array.from(dropdownEl.querySelectorAll('li[data-value]')).forEach((itemEl) => { const { dataset: { value }, } = itemEl; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index d446e32394b..3c630c26bc7 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -23,7 +23,7 @@ export default class FilteredSearchDropdownManager { isGroupDecendent, filteredSearchTokenKeys, }) { - const removeTrailingSlash = url => url.replace(/\/$/, ''); + const removeTrailingSlash = (url) => url.replace(/\/$/, ''); this.container = FilteredSearchContainer.container; this.runnerTagsEndpoint = removeTrailingSlash(runnerTagsEndpoint); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 921d686bb28..11b2eb839ce 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -90,14 +90,14 @@ export default class FilteredSearchManager { // Fetch recent searches from localStorage this.fetchingRecentSearchesPromise = this.recentSearchesService .fetch() - .catch(error => { + .catch((error) => { if (error.name === 'RecentSearchesServiceError') return undefined; // eslint-disable-next-line no-new new Flash(__('An error occurred while parsing recent searches')); // Gracefully fail to empty array return []; }) - .then(searches => { + .then((searches) => { if (!searches) { return; } @@ -169,7 +169,7 @@ export default class FilteredSearchManager { if (this.stateFilters) { this.searchStateWrapper = this.searchState.bind(this); - this.applyToStateFilters(filterEl => { + this.applyToStateFilters((filterEl) => { filterEl.addEventListener('click', this.searchStateWrapper); }); } @@ -177,14 +177,14 @@ export default class FilteredSearchManager { unbindStateEvents() { if (this.stateFilters) { - this.applyToStateFilters(filterEl => { + this.applyToStateFilters((filterEl) => { filterEl.removeEventListener('click', this.searchStateWrapper); }); } } applyToStateFilters(callback) { - this.stateFilters.querySelectorAll('a[data-state]').forEach(filterEl => { + this.stateFilters.querySelectorAll('a[data-state]').forEach((filterEl) => { if (this.states.indexOf(filterEl.dataset.state) > -1) { callback(filterEl); } @@ -263,7 +263,7 @@ export default class FilteredSearchManager { let backspaceCount = 0; // closure for keeping track of the number of backspace keystrokes - return e => { + return (e) => { // 8 = Backspace Key // 46 = Delete Key // Handled by respective backspace-combination check functions @@ -447,7 +447,7 @@ export default class FilteredSearchManager { const removeElements = []; - [].forEach.call(this.tokensContainer.children, t => { + [].forEach.call(this.tokensContainer.children, (t) => { let canClearToken = t.classList.contains('js-visual-token'); if (canClearToken) { @@ -460,7 +460,7 @@ export default class FilteredSearchManager { } }); - removeElements.forEach(el => { + removeElements.forEach((el) => { el.parentElement.removeChild(el); }); @@ -488,7 +488,7 @@ export default class FilteredSearchManager { const { isLastVisualTokenValid } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); if (isLastVisualTokenValid) { - tokens.forEach(t => { + tokens.forEach((t) => { input.value = input.value.replace(`${t.key}:${t.operator}${t.symbol}${t.value}`, ''); FilteredSearchVisualTokens.addFilterVisualToken( @@ -585,7 +585,7 @@ export default class FilteredSearchManager { */ const notKeyValueRegex = new RegExp(/not\[(\w+)\]\[?\]?=(.*)/); - return params.map(query => { + return params.map((query) => { // Check if there are matches for `not` operator const matches = query.match(notKeyValueRegex); if (matches && matches.length === 3) { @@ -624,7 +624,7 @@ export default class FilteredSearchManager { const usernameParams = this.getUsernameParams(); let hasFilteredSearch = false; - params.forEach(p => { + params.forEach((p) => { const split = p.split('='); const keyParam = decodeURIComponent(split[0]); const value = split[1]; @@ -746,7 +746,7 @@ export default class FilteredSearchManager { paths.push(`state=${currentState}`); } - tokens.forEach(token => { + tokens.forEach((token) => { const condition = this.filteredSearchTokenKeys.searchByConditionKeyValue( token.key, token.operator, @@ -795,7 +795,7 @@ export default class FilteredSearchManager { if (searchToken) { const sanitized = searchToken .split(' ') - .map(t => encodeURIComponent(t)) + .map((t) => encodeURIComponent(t)) .join('+'); paths.push(`search=${sanitized}`); } @@ -817,7 +817,7 @@ export default class FilteredSearchManager { const usernamesById = {}; try { const attribute = this.filteredSearchInput.getAttribute('data-username-params'); - JSON.parse(attribute).forEach(user => { + JSON.parse(attribute).forEach((user) => { usernamesById[user.id] = user.username; }); } catch (e) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js index 89fc8047b65..6216ab5401d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js @@ -14,7 +14,7 @@ export default class FilteredSearchTokenKeys { } getKeys() { - return this.tokenKeys.map(i => i.key); + return this.tokenKeys.map((i) => i.key); } getAlternatives() { @@ -36,16 +36,16 @@ export default class FilteredSearchTokenKeys { } searchByKey(key) { - return this.tokenKeys.find(tokenKey => tokenKey.key === key) || null; + return this.tokenKeys.find((tokenKey) => tokenKey.key === key) || null; } searchBySymbol(symbol) { - return this.tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null; + return this.tokenKeys.find((tokenKey) => tokenKey.symbol === symbol) || null; } searchByKeyParam(keyParam) { return ( - this.tokenKeysWithAlternative.find(tokenKey => { + this.tokenKeysWithAlternative.find((tokenKey) => { let tokenKeyParam = tokenKey.key; // Replace hyphen with underscore to compare keyParam with tokenKeyParam @@ -62,13 +62,13 @@ export default class FilteredSearchTokenKeys { } searchByConditionUrl(url) { - return this.conditions.find(condition => condition.url === url) || null; + return this.conditions.find((condition) => condition.url === url) || null; } searchByConditionKeyValue(key, operator, value) { return ( this.conditions.find( - condition => + (condition) => condition.tokenKey === key && condition.operator === operator && condition.value.toLowerCase() === value.toLowerCase(), @@ -93,4 +93,13 @@ export default class FilteredSearchTokenKeys { this.tokenKeys.push(confidentialToken); this.tokenKeysWithAlternative.push(confidentialToken); } + + removeTokensForKeys(...keys) { + const keysSet = new Set(keys); + + this.tokenKeys = this.tokenKeys.filter(({ key }) => !keysSet.has(key)); + this.tokenKeysWithAlternative = this.tokenKeysWithAlternative.filter( + ({ key }) => !keysSet.has(key), + ); + } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index f0951f6b177..4e594dfa910 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -8,7 +8,7 @@ export default class FilteredSearchVisualTokens { static getOperatorToken(value) { let token = null; - FilteredSearchVisualTokens.permissibleOperatorValues.forEach(operatorToken => { + FilteredSearchVisualTokens.permissibleOperatorValues.forEach((operatorToken) => { if (value.startsWith(operatorToken)) { token = operatorToken; } @@ -20,7 +20,7 @@ export default class FilteredSearchVisualTokens { static getValueToken(value) { let newValue = value; - FilteredSearchVisualTokens.permissibleOperatorValues.forEach(operatorToken => { + FilteredSearchVisualTokens.permissibleOperatorValues.forEach((operatorToken) => { if (value.startsWith(operatorToken)) { newValue = value.slice(operatorToken.length); } @@ -48,7 +48,7 @@ export default class FilteredSearchVisualTokens { const otherTokens = FilteredSearchContainer.container.querySelectorAll( '.js-visual-token .selectable.selected', ); - [].forEach.call(otherTokens, t => t.classList.remove('selected')); + [].forEach.call(otherTokens, (t) => t.classList.remove('selected')); } static selectToken(tokenButton, forceSelection = false) { @@ -193,7 +193,8 @@ export default class FilteredSearchVisualTokens { }); } else if ( !isLastVisualTokenValid && - (lastVisualToken && !lastVisualToken.querySelector('.operator')) + lastVisualToken && + !lastVisualToken.querySelector('.operator') ) { const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container'); tokensContainer.removeChild(lastVisualToken); diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js index f9388e9c5d8..46867b184c8 100644 --- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js @@ -150,7 +150,7 @@ export const conditions = flattenDeep( tokenKey: 'my-reaction', value: __('Any'), }, - ].map(condition => { + ].map((condition) => { const [keyPart, valuePart] = condition.url.split('='); const hasBrackets = keyPart.includes('[]'); diff --git a/app/assets/javascripts/filtered_search/stores/recent_searches_store.js b/app/assets/javascripts/filtered_search/stores/recent_searches_store.js index 423f123f71c..ca0a58137b1 100644 --- a/app/assets/javascripts/filtered_search/stores/recent_searches_store.js +++ b/app/assets/javascripts/filtered_search/stores/recent_searches_store.js @@ -19,7 +19,7 @@ class RecentSearchesStore { } setRecentSearches(searches = []) { - const trimmedSearches = searches.map(search => + const trimmedSearches = searches.map((search) => typeof search === 'string' ? search.trim() : search, ); diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js index f73646da6d1..0d36126943b 100644 --- a/app/assets/javascripts/filtered_search/visual_token_value.js +++ b/app/assets/javascripts/filtered_search/visual_token_value.js @@ -40,7 +40,7 @@ export default class VisualTokenValue { return ( UsersCache.retrieve(username) - .then(user => { + .then((user) => { if (!user) { return; } @@ -68,9 +68,9 @@ export default class VisualTokenValue { ); return AjaxCache.retrieve(labelsEndpointWithParams) - .then(labels => { + .then((labels) => { const matchingLabel = (labels || []).find( - label => `~${DropdownUtils.getEscapedText(label.title)}` === tokenValue, + (label) => `~${DropdownUtils.getEscapedText(label.title)}` === tokenValue, ); if (!matchingLabel) { @@ -96,8 +96,8 @@ export default class VisualTokenValue { ); return AjaxCache.retrieve(epicsEndpointWithParams) - .then(epics => { - const matchingEpic = (epics || []).find(epic => epic.id === Number(tokenValue)); + .then((epics) => { + const matchingEpic = (epics || []).find((epic) => epic.id === Number(tokenValue)); if (!matchingEpic) { return; diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index 1d5f09a265b..d14af53746e 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -34,7 +34,7 @@ const hideFlash = (flashEl, fadeTransition = true) => { if (!fadeTransition) flashEl.dispatchEvent(new Event('transitionend')); }; -const createAction = config => ` +const createAction = (config) => ` <a href="${config.href || '#'}" class="flash-action" @@ -69,7 +69,7 @@ const removeFlashClickListener = (flashEl, fadeTransition) => { * @param {String} message Flash message text * @param {String} type Type of Flash, it can be `notice`, `success`, `warning` or `alert` (default) * @param {Object} parent Reference to parent element under which Flash needs to appear - * @param {Object} actonConfig Map of config to show action on banner + * @param {Object} actionConfig Map of config to show action on banner * @param {String} href URL to which action config should point to (default: '#') * @param {String} title Title of action * @param {Function} clickHandler Method to call when action is clicked on @@ -97,7 +97,7 @@ const deprecatedCreateFlash = function deprecatedCreateFlash( if (actionConfig.clickHandler) { flashEl .querySelector('.flash-action') - .addEventListener('click', e => actionConfig.clickHandler(e)); + .addEventListener('click', (e) => actionConfig.clickHandler(e)); } } @@ -119,7 +119,7 @@ const deprecatedCreateFlash = function deprecatedCreateFlash( * @param {String} options.message Flash message text * @param {String} options.type Type of Flash, it can be `notice`, `success`, `warning` or `alert` (default) * @param {Object} options.parent Reference to parent element under which Flash needs to appear - * @param {Object} options.actonConfig Map of config to show action on banner + * @param {Object} options.actionConfig Map of config to show action on banner * @param {String} href URL to which action config should point to (default: '#') * @param {String} title Title of action * @param {Function} clickHandler Method to call when action is clicked on @@ -151,7 +151,7 @@ const createFlash = function createFlash({ if (actionConfig.clickHandler) { flashEl .querySelector('.flash-action') - .addEventListener('click', e => actionConfig.clickHandler(e)); + .addEventListener('click', (e) => actionConfig.clickHandler(e)); } } diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index b9ce0851585..5df0ac37812 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -12,7 +12,7 @@ let sidebar; export const mousePos = []; -export const setSidebar = el => { +export const setSidebar = (el) => { sidebar = el; }; export const getOpenMenu = () => currentOpenMenu; @@ -32,7 +32,7 @@ const setHeaderHeight = () => { export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains(SIDEBAR_COLLAPSED_CLASS); -export const canShowActiveSubItems = el => { +export const canShowActiveSubItems = (el) => { if (el.classList.contains('active') && !isSidebarCollapsed()) { return false; } @@ -71,7 +71,7 @@ export const calculateTop = (boundingRect, outerHeight) => { : boundingRect.top; }; -export const hideMenu = el => { +export const hideMenu = (el) => { if (!el) return; const parentEl = el.parentNode; @@ -112,7 +112,7 @@ export const moveSubItemsToPosition = (el, subItems) => { } }; -export const showSubLevelItems = el => { +export const showSubLevelItems = (el) => { const subItems = el.querySelector('.sidebar-sub-level-items'); const isIconOnly = subItems && subItems.classList.contains('is-fly-out-only'); @@ -139,7 +139,7 @@ export const mouseEnterTopItems = (el, timeout = getHideSubItemsInterval()) => { }, timeout); }; -export const mouseLeaveTopItem = el => { +export const mouseLeaveTopItem = (el) => { const subItems = el.querySelector('.sidebar-sub-level-items'); if ( @@ -152,7 +152,7 @@ export const mouseLeaveTopItem = el => { el.classList.remove(IS_OVER_CLASS); }; -export const documentMouseMove = e => { +export const documentMouseMove = (e) => { mousePos.push({ x: e.clientX, y: e.clientY, @@ -161,7 +161,7 @@ export const documentMouseMove = e => { if (mousePos.length > 6) mousePos.shift(); }; -export const subItemsMouseLeave = relatedTarget => { +export const subItemsMouseLeave = (relatedTarget) => { clearTimeout(timeoutId); if (relatedTarget && !relatedTarget.closest(`.${IS_OVER_CLASS}`)) { @@ -189,15 +189,15 @@ export default () => { requestIdleCallback(setHeaderHeight); - items.forEach(el => { + items.forEach((el) => { const subItems = el.querySelector('.sidebar-sub-level-items'); if (subItems) { - subItems.addEventListener('mouseleave', e => subItemsMouseLeave(e.relatedTarget)); + subItems.addEventListener('mouseleave', (e) => subItemsMouseLeave(e.relatedTarget)); } - el.addEventListener('mouseenter', e => mouseEnterTopItems(e.currentTarget)); - el.addEventListener('mouseleave', e => mouseLeaveTopItem(e.currentTarget)); + el.addEventListener('mouseenter', (e) => mouseEnterTopItems(e.currentTarget)); + el.addEventListener('mouseleave', (e) => mouseLeaveTopItem(e.currentTarget)); }); document.addEventListener('mousemove', documentMouseMove); diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue index c4f61b839e4..68cc8645813 100644 --- a/app/assets/javascripts/frequent_items/components/app.vue +++ b/app/assets/javascripts/frequent_items/components/app.vue @@ -77,7 +77,7 @@ export default { // Check if item already exists in list const itemMatchIndex = storedFrequentItems.findIndex( - frequentItem => frequentItem.id === item.id, + (frequentItem) => frequentItem.id === item.id, ); if (itemMatchIndex > -1) { diff --git a/app/assets/javascripts/frequent_items/index.js b/app/assets/javascripts/frequent_items/index.js index 639562bf961..cef8be37a40 100644 --- a/app/assets/javascripts/frequent_items/index.js +++ b/app/assets/javascripts/frequent_items/index.js @@ -18,7 +18,7 @@ const frequentItemDropdowns = [ ]; export default function initFrequentItemDropdowns() { - frequentItemDropdowns.forEach(dropdown => { + frequentItemDropdowns.forEach((dropdown) => { const { namespace, key } = dropdown; const el = document.getElementById(`js-${namespace}-dropdown`); const navEl = document.getElementById(`nav-${namespace}-dropdown`); diff --git a/app/assets/javascripts/frequent_items/store/actions.js b/app/assets/javascripts/frequent_items/store/actions.js index d4756e2ea6a..f4156487625 100644 --- a/app/assets/javascripts/frequent_items/store/actions.js +++ b/app/assets/javascripts/frequent_items/store/actions.js @@ -1,7 +1,7 @@ -import Api from '~/api'; import AccessorUtilities from '~/lib/utils/accessor'; import * as types from './mutation_types'; import { getTopFrequentItems } from '../utils'; +import { getGroups, getProjects } from '~/rest_api'; export const setNamespace = ({ commit }, namespace) => { commit(types.SET_NAMESPACE, namespace); @@ -54,12 +54,16 @@ export const fetchSearchedItems = ({ state, dispatch }, searchQuery) => { membership: Boolean(gon.current_user_id), }; + let searchFunction; if (state.namespace === 'projects') { + searchFunction = getProjects; params.order_by = 'last_activity_at'; + } else { + searchFunction = getGroups; } - return Api[state.namespace](searchQuery, params) - .then(results => { + return searchFunction(searchQuery, params) + .then((results) => { dispatch('receiveSearchedItemsSuccess', results); }) .catch(() => { diff --git a/app/assets/javascripts/frequent_items/store/getters.js b/app/assets/javascripts/frequent_items/store/getters.js index 36cc9020d8d..e52678dbec2 100644 --- a/app/assets/javascripts/frequent_items/store/getters.js +++ b/app/assets/javascripts/frequent_items/store/getters.js @@ -1 +1 @@ -export const hasSearchQuery = state => state.searchQuery !== ''; +export const hasSearchQuery = (state) => state.searchQuery !== ''; diff --git a/app/assets/javascripts/frequent_items/store/mutations.js b/app/assets/javascripts/frequent_items/store/mutations.js index 78ccef7f253..eee00243867 100644 --- a/app/assets/javascripts/frequent_items/store/mutations.js +++ b/app/assets/javascripts/frequent_items/store/mutations.js @@ -50,7 +50,7 @@ export default { [types.RECEIVE_SEARCHED_ITEMS_SUCCESS](state, results) { const rawItems = results.data ? results.data : results; // Api.groups returns array, Api.projects returns object Object.assign(state, { - items: rawItems.map(rawItem => ({ + items: rawItems.map((rawItem) => ({ id: rawItem.id, name: rawItem.name, namespace: rawItem.name_with_namespace || rawItem.full_name, diff --git a/app/assets/javascripts/frequent_items/utils.js b/app/assets/javascripts/frequent_items/utils.js index 954d426c86c..63fe0ef20b0 100644 --- a/app/assets/javascripts/frequent_items/utils.js +++ b/app/assets/javascripts/frequent_items/utils.js @@ -5,7 +5,7 @@ import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants'; export const isMobile = () => ['md', 'sm', 'xs'].includes(bp.getBreakpointSize()); -export const getTopFrequentItems = items => { +export const getTopFrequentItems = (items) => { if (!items) { return []; } @@ -13,7 +13,7 @@ export const getTopFrequentItems = items => { ? FREQUENT_ITEMS.LIST_COUNT_MOBILE : FREQUENT_ITEMS.LIST_COUNT_DESKTOP; - const frequentItems = items.filter(item => item.frequency >= FREQUENT_ITEMS.ELIGIBLE_FREQUENCY); + const frequentItems = items.filter((item) => item.frequency >= FREQUENT_ITEMS.ELIGIBLE_FREQUENCY); if (!frequentItems || frequentItems.length === 0) { return []; @@ -45,9 +45,9 @@ export const updateExistingFrequentItem = (frequentItem, item) => { }; }; -export const sanitizeItem = item => { +export const sanitizeItem = (item) => { // Only sanitize if the key exists on the item - const maybeSanitize = key => { + const maybeSanitize = (key) => { if (!Object.prototype.hasOwnProperty.call(item, key)) { return {}; } diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index dcb27434a07..cf9ff87f25e 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -15,7 +15,7 @@ function sanitize(str) { } export function membersBeforeSave(members) { - return members.map(member => { + return members.map((member) => { const GROUP_TYPE = 'Group'; let title = ''; @@ -103,6 +103,7 @@ class GfmAutoComplete { at: '/', alias: 'commands', searchKey: 'search', + limit: 100, skipSpecialCharacterTest: true, skipMarkdownCharacterTest: true, data: GfmAutoComplete.defaultLoadingData, @@ -156,7 +157,7 @@ class GfmAutoComplete { ...this.getDefaultCallbacks(), beforeSave(commands) { if (GfmAutoComplete.isLoading(commands)) return commands; - return $.map(commands, c => { + return $.map(commands, (c) => { let search = c.name; if (c.aliases.length > 0) { search = `${search} ${c.aliases.join(' ')}`; @@ -237,7 +238,7 @@ class GfmAutoComplete { }); // return to the form atwho wants - return results.map(name => ({ name })); + return results.map((name) => ({ name })); }, }, }); @@ -285,13 +286,10 @@ class GfmAutoComplete { ...this.getDefaultCallbacks(), beforeSave: membersBeforeSave, matcher(flag, subtext) { - const subtextNodes = subtext - .split(/\n+/g) - .pop() - .split(GfmAutoComplete.regexSubtext); + const subtextNodes = subtext.split(/\n+/g).pop().split(GfmAutoComplete.regexSubtext); // Check if @ is followed by '/assign', '/reassign', '/unassign' or '/cc' commands. - command = subtextNodes.find(node => { + command = subtextNodes.find((node) => { if (Object.values(MEMBER_COMMAND).includes(node)) { return node; } @@ -301,7 +299,7 @@ class GfmAutoComplete { // Cache assignees list for easier filtering later assignees = SidebarMediator.singleton?.store?.assignees?.map( - assignee => `${assignee.username} ${assignee.name}`, + (assignee) => `${assignee.username} ${assignee.name}`, ) || []; const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers); @@ -319,10 +317,10 @@ class GfmAutoComplete { if (command === MEMBER_COMMAND.ASSIGN) { // Only include members which are not assigned to Issuable currently - return data.filter(member => !assignees.includes(member.search)); + return data.filter((member) => !assignees.includes(member.search)); } else if (command === MEMBER_COMMAND.UNASSIGN) { // Only include members which are assigned to Issuable currently - return data.filter(member => assignees.includes(member.search)); + return data.filter((member) => assignees.includes(member.search)); } return data; @@ -349,7 +347,7 @@ class GfmAutoComplete { callbacks: { ...this.getDefaultCallbacks(), beforeSave(issues) { - return $.map(issues, i => { + return $.map(issues, (i) => { if (i.title == null) { return i; } @@ -383,7 +381,7 @@ class GfmAutoComplete { callbacks: { ...this.getDefaultCallbacks(), beforeSave(milestones) { - return $.map(milestones, m => { + return $.map(milestones, (m) => { if (m.title == null) { return m; } @@ -416,7 +414,7 @@ class GfmAutoComplete { callbacks: { ...this.getDefaultCallbacks(), beforeSave(merges) { - return $.map(merges, m => { + return $.map(merges, (m) => { if (m.title == null) { return m; } @@ -457,7 +455,7 @@ class GfmAutoComplete { ...this.getDefaultCallbacks(), beforeSave(merges) { if (GfmAutoComplete.isLoading(merges)) return merges; - return $.map(merges, m => ({ + return $.map(merges, (m) => ({ title: sanitize(m.title), color: m.color, search: m.title, @@ -465,13 +463,10 @@ class GfmAutoComplete { })); }, matcher(flag, subtext) { - const subtextNodes = subtext - .split(/\n+/g) - .pop() - .split(GfmAutoComplete.regexSubtext); + const subtextNodes = subtext.split(/\n+/g).pop().split(GfmAutoComplete.regexSubtext); // Check if ~ is followed by '/label', '/relabel' or '/unlabel' commands. - command = subtextNodes.find(node => { + command = subtextNodes.find((node) => { if ( node === LABEL_COMMAND.LABEL || node === LABEL_COMMAND.RELABEL || @@ -492,7 +487,7 @@ class GfmAutoComplete { return null; } const lastCandidate = subtext.split(flag).pop(); - if (labels.find(label => label.title.startsWith(lastCandidate))) { + if (labels.find((label) => label.title.startsWith(lastCandidate))) { return lastCandidate; } } else { @@ -519,10 +514,10 @@ class GfmAutoComplete { // because we want to return all the labels (unfiltered) for that command. if (command === LABEL_COMMAND.LABEL) { // Return labels with set: undefined. - return data.filter(label => !label.set); + return data.filter((label) => !label.set); } else if (command === LABEL_COMMAND.UNLABEL) { // Return labels with set: true. - return data.filter(label => label.set); + return data.filter((label) => label.set); } return data; @@ -549,7 +544,7 @@ class GfmAutoComplete { callbacks: { ...this.getDefaultCallbacks(), beforeSave(snippets) { - return $.map(snippets, m => { + return $.map(snippets, (m) => { if (m.title == null) { return m; } @@ -654,7 +649,7 @@ class GfmAutoComplete { this.loadEmojiData($input, at).catch(() => {}); } else if (dataSource) { AjaxCache.retrieve(dataSource, true) - .then(data => { + .then((data) => { this.loadData($input, at, data); }) .catch(() => { @@ -696,11 +691,11 @@ class GfmAutoComplete { } lookup[key].push({ kind, emoji }); }; - Object.values(emojis).forEach(emoji => { + Object.values(emojis).forEach((emoji) => { add(emoji.name, 'name', emoji); add(emoji.d, 'description', emoji); add(emoji.e, 'unicode', emoji); - emoji.aliases.forEach(a => add(a, 'alias', emoji)); + emoji.aliases.forEach((a) => add(a, 'alias', emoji)); }); this.emojiLookup = lookup; @@ -772,7 +767,7 @@ GfmAutoComplete.atTypeMap = { }; GfmAutoComplete.typesWithBackendFiltering = ['vulnerabilities']; -GfmAutoComplete.isTypeWithBackendFiltering = type => +GfmAutoComplete.isTypeWithBackendFiltering = (type) => GfmAutoComplete.typesWithBackendFiltering.includes(GfmAutoComplete.atTypeMap[type]); function findEmoji(name) { diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js index ad79483d5ec..eec7a138ea7 100644 --- a/app/assets/javascripts/gl_field_errors.js +++ b/app/assets/javascripts/gl_field_errors.js @@ -16,14 +16,14 @@ export default class GlFieldErrors { initValidators() { // register selectors here as needed const validateSelectors = [':text', ':password', '[type=email]', '[type=url]', '[type=number]'] - .map(selector => `input${selector}`) + .map((selector) => `input${selector}`) .join(','); this.state.inputs = this.form .find(validateSelectors) .toArray() - .filter(input => !input.classList.contains(customValidationFlag)) - .map(input => new GlFieldError({ input, formErrors: this })); + .filter((input) => !input.classList.contains(customValidationFlag)) + .map((input) => new GlFieldError({ input, formErrors: this })); this.form.on('submit', GlFieldErrors.catchInvalidFormSubmit); } @@ -45,7 +45,7 @@ export default class GlFieldErrors { /* Public method for triggering validity updates manually */ updateFormValidityState() { - this.state.inputs.forEach(field => { + this.state.inputs.forEach((field) => { if (field.state.submitted) { field.updateValidity(); } diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index 4a3755f39cc..3e777c2dc09 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -20,7 +20,7 @@ export default class GLForm { // Disable autocomplete for keywords which do not have dataSources available const dataSources = (gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources) || {}; - Object.keys(this.enableGFM).forEach(item => { + Object.keys(this.enableGFM).forEach((item) => { if (item !== 'emojis' && !dataSources[item]) { this.enableGFM[item] = false; } @@ -67,6 +67,7 @@ export default class GLForm { addMarkdownListeners(this.form); this.form.show(); if (this.isAutosizeable) this.setupAutosize(); + if (this.textarea.data('autofocus') === true) this.textarea.focus(); } setupAutosize() { @@ -108,14 +109,10 @@ export default class GLForm { addEventListeners() { this.textarea.on('focus', function focusTextArea() { - $(this) - .closest('.md-area') - .addClass('is-focused'); + $(this).closest('.md-area').addClass('is-focused'); }); this.textarea.on('blur', function blurTextArea() { - $(this) - .closest('.md-area') - .removeClass('is-focused'); + $(this).closest('.md-area').removeClass('is-focused'); }); } diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js index e0f64c8e843..3a8ae56bb8f 100644 --- a/app/assets/javascripts/gpg_badges.js +++ b/app/assets/javascripts/gpg_badges.js @@ -28,7 +28,7 @@ export default class GpgBadges { return axios .get(endpoint, { params }) .then(({ data }) => { - data.signatures.forEach(signature => { + data.signatures.forEach((signature) => { badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html); }); }) diff --git a/app/assets/javascripts/grafana_integration/store/actions.js b/app/assets/javascripts/grafana_integration/store/actions.js index d28e59925d4..436f92eae84 100644 --- a/app/assets/javascripts/grafana_integration/store/actions.js +++ b/app/assets/javascripts/grafana_integration/store/actions.js @@ -24,7 +24,7 @@ export const updateGrafanaIntegration = ({ state, dispatch }) => }, }) .then(() => dispatch('receiveGrafanaIntegrationUpdateSuccess')) - .catch(error => dispatch('receiveGrafanaIntegrationUpdateError', error)); + .catch((error) => dispatch('receiveGrafanaIntegrationUpdateError', error)); export const receiveGrafanaIntegrationUpdateSuccess = () => { /** diff --git a/app/assets/javascripts/grafana_integration/store/index.js b/app/assets/javascripts/grafana_integration/store/index.js index e96bb1e8aad..e3dcfd31a83 100644 --- a/app/assets/javascripts/grafana_integration/store/index.js +++ b/app/assets/javascripts/grafana_integration/store/index.js @@ -6,7 +6,7 @@ import mutations from './mutations'; Vue.use(Vuex); -export const createStore = initialState => +export const createStore = (initialState) => new Vuex.Store({ state: createState(initialState), actions, diff --git a/app/assets/javascripts/alert_management/graphql/fragments/list_item.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql index 62119177887..62119177887 100644 --- a/app/assets/javascripts/alert_management/graphql/fragments/list_item.fragment.graphql +++ b/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql diff --git a/app/assets/javascripts/alert_management/graphql/fragments/alert_note.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/alert_note.fragment.graphql index 74b425717a0..74b425717a0 100644 --- a/app/assets/javascripts/alert_management/graphql/fragments/alert_note.fragment.graphql +++ b/app/assets/javascripts/graphql_shared/fragments/alert_note.fragment.graphql diff --git a/app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.mutation.graphql b/app/assets/javascripts/graphql_shared/mutations/update_alert_status.mutation.graphql index ba1e607bc10..42dc388c9d1 100644 --- a/app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.mutation.graphql +++ b/app/assets/javascripts/graphql_shared/mutations/update_alert_status.mutation.graphql @@ -1,4 +1,4 @@ -#import "../fragments/alert_note.fragment.graphql" +#import "~/graphql_shared/fragments/alert_note.fragment.graphql" mutation updateAlertStatus($projectPath: ID!, $status: AlertManagementStatus!, $iid: String!) { updateAlertStatus(input: { iid: $iid, status: $status, projectPath: $projectPath }) { diff --git a/app/assets/javascripts/alert_management/graphql/queries/get_alerts.query.graphql b/app/assets/javascripts/graphql_shared/queries/get_alerts.query.graphql index bc7e51a2e90..e94758ef60e 100644 --- a/app/assets/javascripts/alert_management/graphql/queries/get_alerts.query.graphql +++ b/app/assets/javascripts/graphql_shared/queries/get_alerts.query.graphql @@ -1,4 +1,4 @@ -#import "../fragments/list_item.fragment.graphql" +#import "~/graphql_shared/fragments/alert.fragment.graphql" query getAlerts( $projectPath: ID! diff --git a/app/assets/javascripts/graphql_shared/utils.js b/app/assets/javascripts/graphql_shared/utils.js index 813e21b6ce9..4715bbc94f6 100644 --- a/app/assets/javascripts/graphql_shared/utils.js +++ b/app/assets/javascripts/graphql_shared/utils.js @@ -51,4 +51,4 @@ export const convertToGraphQLId = (type, id) => { * @param {Array} ids An array of id values * @returns {Array} */ -export const convertToGraphQLIds = (type, ids) => ids.map(id => convertToGraphQLId(type, id)); +export const convertToGraphQLIds = (type, ids) => ids.map((id) => convertToGraphQLId(type, id)); diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js index a840e995860..6878635b288 100644 --- a/app/assets/javascripts/group.js +++ b/app/assets/javascripts/group.js @@ -12,7 +12,7 @@ export default class Group { this.resetHandler = this.reset.bind(this); this.updateGroupPathSlugHandler = this.updateGroupPathSlug.bind(this); - this.groupNames.forEach(groupName => { + this.groupNames.forEach((groupName) => { if (groupName.value === '') { groupName.addEventListener('keyup', this.updateHandler); @@ -22,7 +22,7 @@ export default class Group { } }); - this.groupPaths.forEach(groupPath => { + this.groupPaths.forEach((groupPath) => { groupPath.addEventListener('keydown', this.resetHandler); }); } @@ -30,21 +30,21 @@ export default class Group { update({ currentTarget: { value: updatedValue } }) { const slug = slugify(updatedValue); - this.groupNames.forEach(element => { + this.groupNames.forEach((element) => { element.value = updatedValue; }); - this.groupPaths.forEach(element => { + this.groupPaths.forEach((element) => { element.value = slug; }); } reset() { - this.groupNames.forEach(groupName => { + this.groupNames.forEach((groupName) => { groupName.removeEventListener('keyup', this.updateHandler); groupName.removeEventListener('blur', this.checkPathHandler); }); - this.groupPaths.forEach(groupPath => { + this.groupPaths.forEach((groupPath) => { groupPath.removeEventListener('keydown', this.resetHandler); }); } @@ -59,7 +59,7 @@ export default class Group { if (exists && suggests.length) { const [suggestedSlug] = suggests; - this.groupPaths.forEach(element => { + this.groupPaths.forEach((element) => { element.value = suggestedSlug; }); } else if (exists && !suggests.length) { diff --git a/app/assets/javascripts/group_settings/components/shared_runners_form.vue b/app/assets/javascripts/group_settings/components/shared_runners_form.vue index e396521ce7c..d6ed08a9407 100644 --- a/app/assets/javascripts/group_settings/components/shared_runners_form.vue +++ b/app/assets/javascripts/group_settings/components/shared_runners_form.vue @@ -82,7 +82,7 @@ export default { .then(() => { this.isLoading = false; }) - .catch(error => { + .catch((error) => { const message = [ error.response?.data?.error || __('An error occurred while updating configuration.'), ERROR_MESSAGE, diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue index e057012a246..4f26bab8bd3 100644 --- a/app/assets/javascripts/groups/components/app.vue +++ b/app/assets/javascripts/groups/components/app.vue @@ -1,8 +1,6 @@ <script> /* global Flash */ -import $ from 'jquery'; -import 'vendor/jquery.scrollTo'; import { GlLoadingIcon, GlModal } from '@gitlab/ui'; import { __, s__, sprintf } from '~/locale'; import { HIDDEN_CLASS } from '~/lib/utils/constants'; @@ -108,7 +106,7 @@ export default { fetchGroups({ parentId, page, filterGroupsBy, sortBy, archived, updatePagination }) { return this.service .getGroups(parentId, page, filterGroupsBy, sortBy, archived) - .then(res => { + .then((res) => { if (updatePagination) { this.updatePagination(res.headers); } @@ -116,7 +114,7 @@ export default { }) .catch(() => { this.isLoading = false; - $.scrollTo(0); + window.scrollTo({ top: 0, behavior: 'smooth' }); Flash(COMMON_STR.FAILURE); }); @@ -135,7 +133,7 @@ export default { sortBy, archived, updatePagination: true, - }).then(res => { + }).then((res) => { this.isLoading = false; this.updateGroups(res, Boolean(filterGroupsBy)); }); @@ -149,9 +147,9 @@ export default { sortBy, archived, updatePagination: true, - }).then(res => { + }).then((res) => { this.isLoading = false; - $.scrollTo(0); + window.scrollTo({ top: 0, behavior: 'smooth' }); const currentPath = mergeUrlParams({ page }, window.location.href); window.history.replaceState( @@ -173,7 +171,7 @@ export default { this.fetchGroups({ parentId: parentGroup.id, }) - .then(res => { + .then((res) => { this.store.setGroupChildren(parentGroup, res); }) .catch(() => { @@ -194,12 +192,12 @@ export default { this.targetGroup.isBeingRemoved = true; this.service .leaveGroup(this.targetGroup.leavePath) - .then(res => { - $.scrollTo(0); + .then((res) => { + window.scrollTo({ top: 0, behavior: 'smooth' }); this.store.removeGroup(this.targetGroup, this.targetParentGroup); this.$toast.show(res.data.notice); }) - .catch(err => { + .catch((err) => { let message = COMMON_STR.FAILURE; if (err.status === 403) { message = COMMON_STR.LEAVE_FORBIDDEN; diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index ef58b93c049..d65ad974c73 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -126,7 +126,7 @@ export default { /> <div :class="{ 'd-sm-flex': !group.isChildrenLoading }" - class="avatar-container rect-avatar s32 d-none flex-grow-0 flex-shrink-0 " + class="avatar-container rect-avatar s32 d-none flex-grow-0 flex-shrink-0" > <a :href="group.relativePath" class="no-expand"> <img diff --git a/app/assets/javascripts/groups/components/visibility_level_dropdown.vue b/app/assets/javascripts/groups/components/visibility_level_dropdown.vue index ff0f8c3ff46..0933045fc38 100644 --- a/app/assets/javascripts/groups/components/visibility_level_dropdown.vue +++ b/app/assets/javascripts/groups/components/visibility_level_dropdown.vue @@ -23,7 +23,7 @@ export default { }, methods: { getDefaultOption() { - return this.visibilityLevelOptions.find(option => option.level === this.defaultLevel); + return this.visibilityLevelOptions.find((option) => option.level === this.defaultLevel); }, onClick(option) { this.selectedOption = option; diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js index 693519729ac..c33ad8b6ecb 100644 --- a/app/assets/javascripts/groups/groups_filterable_list.js +++ b/app/assets/javascripts/groups/groups_filterable_list.js @@ -65,10 +65,7 @@ export default class GroupFilterableList extends FilterableList { setDefaultFilterOption() { const defaultOption = $.trim( - this.$dropdown - .find('.dropdown-menu li.js-filter-sort-order a') - .first() - .text(), + this.$dropdown.find('.dropdown-menu li.js-filter-sort-order a').first().text(), ); this.$dropdown.find('.dropdown-label').text(defaultOption); } diff --git a/app/assets/javascripts/groups/init_invite_members_banner.js b/app/assets/javascripts/groups/init_invite_members_banner.js index c7967827917..2052dd6ac8c 100644 --- a/app/assets/javascripts/groups/init_invite_members_banner.js +++ b/app/assets/javascripts/groups/init_invite_members_banner.js @@ -18,6 +18,6 @@ export default function initInviteMembersBanner() { isDismissedKey, trackLabel, }, - render: createElement => createElement(InviteMembersBanner), + render: (createElement) => createElement(InviteMembersBanner), }); } diff --git a/app/assets/javascripts/groups/members/components/app.vue b/app/assets/javascripts/groups/members/components/app.vue index f6f3a955813..34a2c67fa9f 100644 --- a/app/assets/javascripts/groups/members/components/app.vue +++ b/app/assets/javascripts/groups/members/components/app.vue @@ -5,12 +5,10 @@ import MembersTable from '~/members/components/table/members_table.vue'; import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue'; import { scrollToElement } from '~/lib/utils/common_utils'; import { HIDE_ERROR } from '~/members/store/mutation_types'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { name: 'GroupMembersApp', components: { MembersTable, FilterSortContainer, GlAlert }, - mixins: [glFeatureFlagsMixin()], computed: { ...mapState(['showError', 'errorMessage']), }, @@ -36,7 +34,7 @@ export default { <gl-alert v-if="showError" ref="errorAlert" variant="danger" @dismiss="hideError">{{ errorMessage }}</gl-alert> - <filter-sort-container v-if="glFeatures.groupMembersFilteredSearch" /> + <filter-sort-container /> <members-table /> </div> </template> diff --git a/app/assets/javascripts/groups/members/index.js b/app/assets/javascripts/groups/members/index.js index 9ce0e3c1179..3ec874b8d36 100644 --- a/app/assets/javascripts/groups/members/index.js +++ b/app/assets/javascripts/groups/members/index.js @@ -38,6 +38,6 @@ export const initGroupMembersApp = ( el, components: { App }, store, - render: createElement => createElement('app'), + render: (createElement) => createElement('app'), }); }; diff --git a/app/assets/javascripts/groups/members/utils.js b/app/assets/javascripts/groups/members/utils.js index 2d584556bbc..4fcf348b69f 100644 --- a/app/assets/javascripts/groups/members/utils.js +++ b/app/assets/javascripts/groups/members/utils.js @@ -7,7 +7,7 @@ import { GROUP_LINK_ACCESS_LEVEL_PROPERTY_NAME, } from './constants'; -export const parseDataAttributes = el => { +export const parseDataAttributes = (el) => { const { members, groupId, memberPath, canManageMembers } = el.dataset; return { diff --git a/app/assets/javascripts/groups/store/groups_store.js b/app/assets/javascripts/groups/store/groups_store.js index b6cea38e87f..6cf70f4052e 100644 --- a/app/assets/javascripts/groups/store/groups_store.js +++ b/app/assets/javascripts/groups/store/groups_store.js @@ -12,15 +12,15 @@ export default class GroupsStore { setGroups(rawGroups) { if (rawGroups && rawGroups.length) { - this.state.groups = rawGroups.map(rawGroup => this.formatGroupItem(rawGroup)); + this.state.groups = rawGroups.map((rawGroup) => this.formatGroupItem(rawGroup)); } else { this.state.groups = []; } } setSearchedGroups(rawGroups) { - const formatGroups = groups => - groups.map(group => { + const formatGroups = (groups) => + groups.map((group) => { const formattedGroup = this.formatGroupItem(group); if (formattedGroup.children && formattedGroup.children.length) { formattedGroup.children = formatGroups(formattedGroup.children); @@ -37,7 +37,7 @@ export default class GroupsStore { setGroupChildren(parentGroup, children) { const updatedParentGroup = parentGroup; - updatedParentGroup.children = children.map(rawChild => this.formatGroupItem(rawChild)); + updatedParentGroup.children = children.map((rawChild) => this.formatGroupItem(rawChild)); updatedParentGroup.isOpen = true; updatedParentGroup.isChildrenLoading = false; } @@ -103,9 +103,9 @@ export default class GroupsStore { removeGroup(group, parentGroup) { const updatedParentGroup = parentGroup; if (updatedParentGroup.children && updatedParentGroup.children.length) { - updatedParentGroup.children = parentGroup.children.filter(child => group.id !== child.id); + updatedParentGroup.children = parentGroup.children.filter((child) => group.id !== child.id); } else { - this.state.groups = this.state.groups.filter(child => group.id !== child.id); + this.state.groups = this.state.groups.filter((child) => group.id !== child.id); } } } diff --git a/app/assets/javascripts/groups/transfer_dropdown.js b/app/assets/javascripts/groups/transfer_dropdown.js index cefd803c631..59cc779d2ae 100644 --- a/app/assets/javascripts/groups/transfer_dropdown.js +++ b/app/assets/javascripts/groups/transfer_dropdown.js @@ -20,11 +20,11 @@ export default class TransferDropdown { initDeprecatedJQueryDropdown(this.groupDropdown, { selectable: true, filterable: true, - toggleLabel: item => item.text, + toggleLabel: (item) => item.text, search: { fields: ['text'] }, data: extraOptions.concat(this.data), - text: item => item.text, - clicked: options => { + text: (item) => item.text, + clicked: (options) => { const { e } = options; e.preventDefault(); this.assignSelected(options.selectedObj); diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 29af8c77d25..c65fff432d0 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -6,11 +6,11 @@ import { normalizeHeaders } from './lib/utils/common_utils'; import { __ } from '~/locale'; import { loadCSSFile } from './lib/utils/css_utils'; -const fetchGroups = params => { +const fetchGroups = (params) => { axios[params.type.toLowerCase()](params.url, { params: params.data, }) - .then(res => { + .then((res) => { const results = res.data || []; const headers = normalizeHeaders(res.headers); const currentPage = parseInt(headers['X-PAGE'], 10) || 0; @@ -67,7 +67,7 @@ const groupsSelect = () => { const groups = data.length ? data : data.results || []; const more = data.pagination ? data.pagination.more : false; - const results = groups.filter(group => skipGroups.indexOf(group.id) === -1); + const results = groups.filter((group) => skipGroups.indexOf(group.id) === -1); return { results, diff --git a/app/assets/javascripts/helpers/event_hub_factory.js b/app/assets/javascripts/helpers/event_hub_factory.js index a9c301e3a93..62af67d3ef3 100644 --- a/app/assets/javascripts/helpers/event_hub_factory.js +++ b/app/assets/javascripts/helpers/event_hub_factory.js @@ -45,7 +45,7 @@ class EventHub { $off(type, handler) { const handlers = this.$_all.get(type) || []; - const newHandlers = handler ? handlers.filter(x => x !== handler) : []; + const newHandlers = handler ? handlers.filter((x) => x !== handler) : []; if (newHandlers.length) { this.$_all.set(type, newHandlers); @@ -77,7 +77,7 @@ class EventHub { $emit(type, ...args) { const handlers = this.$_all.get(type) || []; - handlers.forEach(handler => { + handlers.forEach((handler) => { handler(...args); }); } diff --git a/app/assets/javascripts/helpers/help_page_helper.js b/app/assets/javascripts/helpers/help_page_helper.js new file mode 100644 index 00000000000..0e824548646 --- /dev/null +++ b/app/assets/javascripts/helpers/help_page_helper.js @@ -0,0 +1,21 @@ +import { joinPaths, setUrlFragment } from '~/lib/utils/url_utility'; + +const HELP_PAGE_URL_ROOT = '/help/'; + +/** + * Generate link to a GitLab documentation page. + * + * This is designed to mirror the Ruby `help_page_path` helper function, so that + * the two can be used interchangeably. + * @param {String} path - Path to doc file relative to the doc/ directory in the GitLab repository. + * Optionally, including `.md` or `.html` prefix + * @param {String} options.anchor - Name of the anchor to scroll to on the documentation page. + */ +export const helpPagePath = (path, { anchor = '' } = {}) => { + let helpPath = joinPaths(gon.relative_url_root || '/', HELP_PAGE_URL_ROOT, path); + if (anchor) { + helpPath = setUrlFragment(helpPath, anchor); + } + + return helpPath; +}; diff --git a/app/assets/javascripts/helpers/monitor_helper.js b/app/assets/javascripts/helpers/monitor_helper.js index 5e345321013..7c6a6d6a433 100644 --- a/app/assets/javascripts/helpers/monitor_helper.js +++ b/app/assets/javascripts/helpers/monitor_helper.js @@ -38,7 +38,7 @@ const templatedLabel = (queryLabel, metricAttributes) => { * @example * multiMetricLabel('', {__name__: "up", app: "prometheus"}) -> "__name__: up, app: prometheus" */ -const multiMetricLabel = metricAttributes => { +const multiMetricLabel = (metricAttributes) => { return Object.entries(metricAttributes) .map(([templateVar, label]) => `${templateVar}: ${label}`) .join(', '); @@ -64,7 +64,7 @@ export const getSeriesLabel = (queryLabel, metricAttributes) => { * @returns {Array} The formatted values */ export const makeDataSeries = (queryResults, defaultConfig) => - queryResults.map(result => { + queryResults.map((result) => { return { ...defaultConfig, data: result.values, diff --git a/app/assets/javascripts/helpers/startup_css_helper.js b/app/assets/javascripts/helpers/startup_css_helper.js index d41a6209898..6e19979721c 100644 --- a/app/assets/javascripts/helpers/startup_css_helper.js +++ b/app/assets/javascripts/helpers/startup_css_helper.js @@ -22,14 +22,14 @@ const handleStartupEvents = () => { /* For `waitForCSSLoaded` methods, see docs.gitlab.com/ee/development/fe_guide/performance.html#important-considerations */ export const waitForCSSLoaded = (action = () => {}) => { - if (!gon?.features?.startupCss || allLinksLoaded()) { - return new Promise(resolve => { + if (allLinksLoaded()) { + return new Promise((resolve) => { action(); resolve(); }); } - return new Promise(resolve => { + return new Promise((resolve) => { document.addEventListener(CSS_LOADED_EVENT, resolve, { once: true }); document.addEventListener(STARTUP_LINK_LOADED_EVENT, handleStartupEvents); }).then(action); diff --git a/app/assets/javascripts/ide/commit_icon.js b/app/assets/javascripts/ide/commit_icon.js index 4984b5bb91d..70ee9cff22b 100644 --- a/app/assets/javascripts/ide/commit_icon.js +++ b/app/assets/javascripts/ide/commit_icon.js @@ -1,6 +1,6 @@ import { commitItemIconMap } from './constants'; -export default file => { +export default (file) => { if (file.deleted) { return commitItemIconMap.deleted; } else if (file.tempFile && !file.prevPath) { diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue index 123e0aba959..4192a002486 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue @@ -63,7 +63,7 @@ export default { return this.openPendingTab({ file: this.file, keyPrefix: this.keyPrefix, - }).then(changeViewer => { + }).then((changeViewer) => { if (changeViewer) { this.updateViewer(viewerTypes.diff); } diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue index aed7b792902..91cce44382c 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue @@ -74,6 +74,7 @@ export default { <input :placeholder="placeholderBranchName" :value="newBranchName" + data-testid="ide-new-branch-name" type="text" class="form-control monospace" @input="updateBranchName($event.target.value)" diff --git a/app/assets/javascripts/ide/components/file_templates/bar.vue b/app/assets/javascripts/ide/components/file_templates/bar.vue index 88dca2f0556..bd4c4f18141 100644 --- a/app/assets/javascripts/ide/components/file_templates/bar.vue +++ b/app/assets/javascripts/ide/components/file_templates/bar.vue @@ -29,7 +29,7 @@ export default { 'undoFileTemplate', ]), setInitialType() { - const initialTemplateType = this.templateTypes.find(t => t.name === this.activeFile.name); + const initialTemplateType = this.templateTypes.find((t) => t.name === this.activeFile.name); if (initialTemplateType) { this.setSelectedTemplateType(initialTemplateType); diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue index 5d5b66a6444..772dab3fed3 100644 --- a/app/assets/javascripts/ide/components/file_templates/dropdown.vue +++ b/app/assets/javascripts/ide/components/file_templates/dropdown.vue @@ -44,7 +44,7 @@ export default { computed: { ...mapState('fileTemplates', ['templates', 'isLoading']), outputData() { - return (this.isAsyncData ? this.templates : this.data).filter(t => { + return (this.isAsyncData ? this.templates : this.data).filter((t) => { if (!this.searchable) return true; return t.name.toLowerCase().indexOf(this.search.toLowerCase()) >= 0; diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index f8568f46cd6..aac899fde0d 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -73,7 +73,7 @@ export default { }, }, mounted() { - window.onbeforeunload = e => this.onBeforeUnload(e); + window.onbeforeunload = (e) => this.onBeforeUnload(e); if (this.themeName) document.querySelector('.navbar-gitlab').classList.add(`theme-${this.themeName}`); diff --git a/app/assets/javascripts/ide/components/ide_sidebar_nav.vue b/app/assets/javascripts/ide/components/ide_sidebar_nav.vue index 966c36d6e71..9dbed0ace40 100644 --- a/app/assets/javascripts/ide/components/ide_sidebar_nav.vue +++ b/app/assets/javascripts/ide/components/ide_sidebar_nav.vue @@ -37,7 +37,7 @@ export default { }, methods: { isActiveTab(tab) { - return this.isOpen && tab.views.some(view => view.name === this.currentView); + return this.isOpen && tab.views.some((view) => view.name === this.currentView); }, buttonClasses(tab) { return [ diff --git a/app/assets/javascripts/ide/components/nav_dropdown.vue b/app/assets/javascripts/ide/components/nav_dropdown.vue index 8cea8655461..6ff77e556c0 100644 --- a/app/assets/javascripts/ide/components/nav_dropdown.vue +++ b/app/assets/javascripts/ide/components/nav_dropdown.vue @@ -31,9 +31,7 @@ export default { }, removeDropdownListeners() { // eslint-disable-next-line @gitlab/no-global-event-off - $(this.$refs.dropdown) - .off('show.bs.dropdown') - .off('hide.bs.dropdown'); + $(this.$refs.dropdown).off('show.bs.dropdown').off('hide.bs.dropdown'); }, showDropdown() { this.isVisibleDropdown = true; diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue index 4a9a2a57acd..5704129c10f 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue @@ -25,23 +25,24 @@ export default { }, methods: { createFile(target, file) { - const { name } = file; + const { name, type: mimeType } = file; const encodedContent = target.result.split('base64,')[1]; const rawContent = encodedContent ? atob(encodedContent) : ''; - const isText = isTextFile({ content: rawContent, mimeType: file.type, name }); + const isText = isTextFile({ content: rawContent, mimeType, name }); - const emitCreateEvent = content => + const emitCreateEvent = (content) => this.$emit('create', { name: `${this.path ? `${this.path}/` : ''}${name}`, type: 'blob', content, rawPath: !isText ? URL.createObjectURL(file) : '', + mimeType, }); if (isText) { const reader = new FileReader(); - reader.addEventListener('load', e => emitCreateEvent(e.target.result), { once: true }); + reader.addEventListener('load', (e) => emitCreateEvent(e.target.result), { once: true }); reader.readAsText(file); } else { emitCreateEvent(rawContent); @@ -50,11 +51,11 @@ export default { readFile(file) { const reader = new FileReader(); - reader.addEventListener('load', e => this.createFile(e.target, file), { once: true }); + reader.addEventListener('load', (e) => this.createFile(e.target, file), { once: true }); reader.readAsDataURL(file); }, openFile() { - Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file)); + Array.from(this.$refs.fileUpload.files).forEach((file) => this.readFile(file)); }, startFileUpload() { this.$refs.fileUpload.click(); @@ -80,6 +81,7 @@ export default { type="file" class="hidden" multiple + data-qa-selector="file_upload_field" @change="openFile" /> </div> diff --git a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue index 87019c3b2a5..6f42ae48cc9 100644 --- a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue +++ b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue @@ -35,13 +35,13 @@ export default { return `${this.side}Pane`; }, tabs() { - return this.extensionTabs.filter(tab => tab.show); + return this.extensionTabs.filter((tab) => tab.show); }, tabViews() { - return this.tabs.map(tab => tab.views).flat(); + return this.tabs.map((tab) => tab.views).flat(); }, aliveTabViews() { - return this.tabViews.filter(view => this.isAliveView(view.name)); + return this.tabViews.filter((view) => this.isAliveView(view.name)); }, }, methods: { diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue index f65b1201d94..4c2a369226e 100644 --- a/app/assets/javascripts/ide/components/preview/clientside.vue +++ b/app/assets/javascripts/ide/components/preview/clientside.vue @@ -1,12 +1,13 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; -import { isEmpty } from 'lodash'; +import { isEmpty, debounce } from 'lodash'; import { Manager } from 'smooshpack'; import { listen } from 'codesandbox-api'; import { GlLoadingIcon } from '@gitlab/ui'; import Navigator from './navigator.vue'; -import { packageJsonPath } from '../../constants'; +import { packageJsonPath, LIVE_PREVIEW_DEBOUNCE } from '../../constants'; import { createPathWithExt } from '../../utils'; +import eventHub from '../../eventhub'; export default { components: { @@ -61,13 +62,10 @@ export default { }; }, }, - watch: { - entries: { - deep: true, - handler: 'update', - }, - }, mounted() { + this.onFilesChangeCallback = debounce(() => this.update(), LIVE_PREVIEW_DEBOUNCE); + eventHub.$on('ide.files.change', this.onFilesChangeCallback); + this.loading = true; return this.loadFileContent(packageJsonPath) @@ -78,17 +76,19 @@ export default { .then(() => this.initPreview()); }, beforeDestroy() { + // Setting sandpackReady = false protects us form a phantom `update()` being called when `debounce` finishes. + this.sandpackReady = false; + eventHub.$off('ide.files.change', this.onFilesChangeCallback); + if (!isEmpty(this.manager)) { this.manager.listener(); } + this.manager = {}; if (this.listener) { this.listener(); } - - clearTimeout(this.timeout); - this.timeout = null; }, methods: { ...mapActions(['getFileData', 'getRawFileData']), @@ -108,7 +108,7 @@ export default { .then(() => { this.initManager(); - this.listener = listen(e => { + this.listener = listen((e) => { switch (e.type) { case 'done': this.sandpackReady = true; @@ -122,25 +122,21 @@ export default { update() { if (!this.sandpackReady) return; - clearTimeout(this.timeout); - - this.timeout = setTimeout(() => { - if (isEmpty(this.manager)) { - this.initPreview(); + if (isEmpty(this.manager)) { + this.initPreview(); - return; - } + return; + } - this.manager.updatePreview(this.sandboxOpts); - }, 250); + this.manager.updatePreview(this.sandboxOpts); }, initManager() { const { codesandboxBundlerUrl: bundlerURL } = this; const settings = { fileResolver: { - isFile: p => Promise.resolve(Boolean(this.entries[createPathWithExt(p)])), - readFile: p => this.loadFileContent(createPathWithExt(p)).then(content => content), + isFile: (p) => Promise.resolve(Boolean(this.entries[createPathWithExt(p)])), + readFile: (p) => this.loadFileContent(createPathWithExt(p)).then((content) => content), }, ...(bundlerURL ? { bundlerURL } : {}), }; diff --git a/app/assets/javascripts/ide/components/preview/navigator.vue b/app/assets/javascripts/ide/components/preview/navigator.vue index 60710251fef..8986359427f 100644 --- a/app/assets/javascripts/ide/components/preview/navigator.vue +++ b/app/assets/javascripts/ide/components/preview/navigator.vue @@ -31,7 +31,7 @@ export default { }, }, mounted() { - this.listener = listen(e => { + this.listener = listen((e) => { switch (e.type) { case 'urlchange': this.onUrlChange(e); diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index dfd25feed08..8092ef3bce6 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -41,12 +41,12 @@ export default { file, keyPrefix: file.staged ? stageKeys.staged : stageKeys.unstaged, }) - .then(changeViewer => { + .then((changeViewer) => { if (changeViewer) { this.updateViewer('diff'); } }) - .catch(e => { + .catch((e) => { throw e; }); }, diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index 1f029612c29..a9c05f2e1ac 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -74,8 +74,11 @@ export default { fileEditor() { return getFileEditorOrDefault(this.fileEditors, this.file.path); }, + isBinaryFile() { + return !isTextFile(this.file); + }, shouldHideEditor() { - return this.file && !this.file.loading && !isTextFile(this.file); + return this.file && !this.file.loading && this.isBinaryFile; }, showContentViewer() { return ( @@ -216,7 +219,7 @@ export default { .then(() => { this.createEditorInstance(); }) - .catch(err => { + .catch((err) => { flash( __('Error setting up editor. Please try again.'), 'alert', @@ -244,6 +247,10 @@ export default { ); }, createEditorInstance() { + if (this.isBinaryFile) { + return; + } + this.editor.dispose(); this.$nextTick(() => { @@ -274,7 +281,7 @@ export default { this.model.updateOptions(this.rules); - this.model.onChange(model => { + this.model.onChange((model) => { const { file } = model; if (!file.active) return; @@ -322,7 +329,7 @@ export default { } }, fetchEditorconfigRules() { - return getRulesWithTraversal(this.file.path, path => { + return getRulesWithTraversal(this.file.path, (path) => { const entry = this.entries[path]; if (!entry) return Promise.resolve(null); @@ -332,7 +339,7 @@ export default { return this.getFileData({ path: entry.path, makeFileActive: false }).then(() => this.getRawFileData({ path: entry.path }), ); - }).then(rules => { + }).then((rules) => { this.rules = mapRulesToMonaco(rules); }); }, @@ -346,7 +353,7 @@ export default { event.preventDefault(); event.stopImmediatePropagation(); - return readFileAsDataURL(file).then(content => { + return readFileAsDataURL(file).then((content) => { const parentPath = getPathParent(this.file.path); const path = `${parentPath ? `${parentPath}/` : ''}${file.name}`; diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index bdb11e6b004..e5618466395 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -97,3 +97,6 @@ export const packageJsonPath = 'package.json'; export const SIDE_LEFT = 'left'; export const SIDE_RIGHT = 'right'; + +// Live Preview feature +export const LIVE_PREVIEW_DEBOUNCE = 2000; diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js index b9ebacef7e1..0e6775d87f1 100644 --- a/app/assets/javascripts/ide/ide_router.js +++ b/app/assets/javascripts/ide/ide_router.js @@ -39,7 +39,7 @@ const EmptyRouterComponent = { }, }; -export const createRouter = store => { +export const createRouter = (store) => { const router = new IdeRouter({ mode: 'history', base: joinPaths(gon.relative_url_root || '', '/-/ide/'), @@ -54,11 +54,11 @@ export const createRouter = store => { }, { path: ':targetmode(edit|tree|blob)/:branchid+/', - redirect: to => joinPaths(to.path, '/-/'), + redirect: (to) => joinPaths(to.path, '/-/'), }, { path: ':targetmode(edit|tree|blob)', - redirect: to => joinPaths(to.path, '/master/-/'), + redirect: (to) => joinPaths(to.path, '/master/-/'), }, { path: 'merge_requests/:mrid', @@ -66,7 +66,7 @@ export const createRouter = store => { }, { path: '', - redirect: to => joinPaths(to.path, '/edit/master/-/'), + redirect: (to) => joinPaths(to.path, '/edit/master/-/'), }, ], }, @@ -110,7 +110,7 @@ export const createRouter = store => { }); } }) - .catch(e => { + .catch((e) => { flash( __('Error while loading the project data. Please try again.'), 'alert', diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index 62f49ba56b1..af408c06556 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -63,6 +63,10 @@ export function initIde(el, options = {}) { codesandboxBundlerUrl: el.dataset.codesandboxBundlerUrl, }); }, + beforeDestroy() { + // This helps tests do Singleton cleanups which we don't really have responsibility to know about here. + this.$emit('destroy'); + }, methods: { ...mapActions(['setEmptyStateSvgs', 'setLinks', 'setInitialData']), }, diff --git a/app/assets/javascripts/ide/lib/common/disposable.js b/app/assets/javascripts/ide/lib/common/disposable.js index 84b29bdb600..c5d0773c9a2 100644 --- a/app/assets/javascripts/ide/lib/common/disposable.js +++ b/app/assets/javascripts/ide/lib/common/disposable.js @@ -4,11 +4,11 @@ export default class Disposable { } add(...disposers) { - disposers.forEach(disposer => this.disposers.add(disposer)); + disposers.forEach((disposer) => this.disposers.add(disposer)); } dispose() { - this.disposers.forEach(disposer => disposer.dispose()); + this.disposers.forEach((disposer) => disposer.dispose()); this.disposers.clear(); } } diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js index 2471b3627ce..4969875439e 100644 --- a/app/assets/javascripts/ide/lib/common/model.js +++ b/app/assets/javascripts/ide/lib/common/model.js @@ -75,7 +75,7 @@ export default class Model { } onChange(cb) { - this.events.add(this.disposable.add(this.model.onDidChangeContent(e => cb(this, e)))); + this.events.add(this.disposable.add(this.model.onDidChangeContent((e) => cb(this, e)))); } onDispose(cb) { @@ -121,7 +121,7 @@ export default class Model { dispose() { if (!this.model.isDisposed()) this.applyCustomOptions(); - this.events.forEach(cb => { + this.events.forEach((cb) => { if (typeof cb === 'function') cb(); }); diff --git a/app/assets/javascripts/ide/lib/create_diff.js b/app/assets/javascripts/ide/lib/create_diff.js index 3e915afdbcb..51d4967fb23 100644 --- a/app/assets/javascripts/ide/lib/create_diff.js +++ b/app/assets/javascripts/ide/lib/create_diff.js @@ -32,8 +32,8 @@ const filesWithChanges = ({ stagedFiles = [], changedFiles = [], entries = {} }) // We need to clean "move" actions, because we can only support 100% similarity moves at the moment. // This is because the previous file's content might not be loaded. Object.values(changes) - .filter(change => change.action === commitActionTypes.move) - .forEach(change => { + .filter((change) => change.action === commitActionTypes.move) + .forEach((change) => { const prev = changes[change.file.prevPath]; if (!prev) { @@ -51,14 +51,14 @@ const filesWithChanges = ({ stagedFiles = [], changedFiles = [], entries = {} }) // Next, we need to add deleted directories by looking at the parents Object.values(changes) - .filter(change => change.action === commitActionTypes.delete && change.file.parentPath) + .filter((change) => change.action === commitActionTypes.delete && change.file.parentPath) .forEach(({ file }) => { // Do nothing if we've already visited this directory. if (changes[file.parentPath]) { return; } - getDeletedParents(entries, file).forEach(parent => { + getDeletedParents(entries, file).forEach((parent) => { changes[parent.path] = { action: commitActionTypes.delete, file: parent }; }); }); @@ -66,13 +66,15 @@ const filesWithChanges = ({ stagedFiles = [], changedFiles = [], entries = {} }) return Object.values(changes); }; -const createDiff = state => { +const createDiff = (state) => { const changes = filesWithChanges(state); - const toDelete = changes.filter(x => x.action === commitActionTypes.delete).map(x => x.file.path); + const toDelete = changes + .filter((x) => x.action === commitActionTypes.delete) + .map((x) => x.file.path); const patch = changes - .filter(x => x.action !== commitActionTypes.delete) + .filter((x) => x.action !== commitActionTypes.delete) .map(({ file, action }) => createFileDiff(file, action)) .join(''); diff --git a/app/assets/javascripts/ide/lib/create_file_diff.js b/app/assets/javascripts/ide/lib/create_file_diff.js index 5ae4993321c..b417b4765d8 100644 --- a/app/assets/javascripts/ide/lib/create_file_diff.js +++ b/app/assets/javascripts/ide/lib/create_file_diff.js @@ -12,13 +12,13 @@ const NEW_LINE = '\n'; * * - Removes "=======" separator added at the beginning */ -const cleanTwoFilesPatch = text => text.replace(/^(=+\s*)/, ''); +const cleanTwoFilesPatch = (text) => text.replace(/^(=+\s*)/, ''); -const endsWithNewLine = val => !val || val[val.length - 1] === NEW_LINE; +const endsWithNewLine = (val) => !val || val[val.length - 1] === NEW_LINE; -const addEndingNewLine = val => (endsWithNewLine(val) ? val : val + NEW_LINE); +const addEndingNewLine = (val) => (endsWithNewLine(val) ? val : val + NEW_LINE); -const removeEndingNewLine = val => (endsWithNewLine(val) ? val.substr(0, val.length - 1) : val); +const removeEndingNewLine = (val) => (endsWithNewLine(val) ? val.substr(0, val.length - 1) : val); const diffHead = (prevPath, newPath = '') => `diff --git "a/${prevPath}" "b/${newPath || prevPath}"`; @@ -37,7 +37,7 @@ const createDiffBody = (path, content, isCreate) => { const chunkHead = isCreate ? `@@ -0,0 +1,${lines.length} @@` : `@@ -1,${lines.length} +0,0 @@`; const chunk = lines - .map(line => `${prefix}${line}`) + .map((line) => `${prefix}${line}`) .concat(!hasNewLine ? [NO_NEW_LINE] : []) .join(NEW_LINE); diff --git a/app/assets/javascripts/ide/lib/decorations/controller.js b/app/assets/javascripts/ide/lib/decorations/controller.js index 13d477bb2cf..b5d3eb10952 100644 --- a/app/assets/javascripts/ide/lib/decorations/controller.js +++ b/app/assets/javascripts/ide/lib/decorations/controller.js @@ -11,7 +11,7 @@ export default class DecorationsController { const modelDecorations = this.decorations.get(model.url); const decorations = []; - modelDecorations.forEach(val => decorations.push(...val)); + modelDecorations.forEach((val) => decorations.push(...val)); return decorations; } diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js index 35fcda6a6c5..3efe692be13 100644 --- a/app/assets/javascripts/ide/lib/diff/controller.js +++ b/app/assets/javascripts/ide/lib/diff/controller.js @@ -3,7 +3,7 @@ import { throttle } from 'lodash'; import DirtyDiffWorker from './diff_worker'; import Disposable from '../common/disposable'; -export const getDiffChangeType = change => { +export const getDiffChangeType = (change) => { if (change.modified) { return 'modified'; } else if (change.added) { @@ -15,7 +15,7 @@ export const getDiffChangeType = change => { return ''; }; -export const getDecorator = change => ({ +export const getDecorator = (change) => ({ range: new Range(change.lineNumber, 1, change.endLineNumber, 1), options: { isWholeLine: true, @@ -71,7 +71,7 @@ export default class DirtyDiffController { } decorate({ data }) { - const decorations = data.changes.map(change => getDecorator(change)); + const decorations = data.changes.map((change) => getDecorator(change)); const model = this.modelManager.getModel(data.path); this.decorationsController.addDecorations(model, 'dirtyDiff', decorations); } diff --git a/app/assets/javascripts/ide/lib/diff/diff.js b/app/assets/javascripts/ide/lib/diff/diff.js index 62ec798b372..5a6401f56ec 100644 --- a/app/assets/javascripts/ide/lib/diff/diff.js +++ b/app/assets/javascripts/ide/lib/diff/diff.js @@ -11,7 +11,7 @@ export const computeDiff = (originalContent, newContent) => { let lineNumber = 1; return changes.reduce((acc, change) => { - const findOnLine = acc.find(c => c.lineNumber === lineNumber); + const findOnLine = acc.find((c) => c.lineNumber === lineNumber); if (findOnLine) { Object.assign(findOnLine, change, { diff --git a/app/assets/javascripts/ide/lib/diff/diff_worker.js b/app/assets/javascripts/ide/lib/diff/diff_worker.js index 77416a8de9d..78b2eab6399 100644 --- a/app/assets/javascripts/ide/lib/diff/diff_worker.js +++ b/app/assets/javascripts/ide/lib/diff/diff_worker.js @@ -1,7 +1,7 @@ import { computeDiff } from './diff'; // eslint-disable-next-line no-restricted-globals -self.addEventListener('message', e => { +self.addEventListener('message', (e) => { const { data } = e; // eslint-disable-next-line no-restricted-globals diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js index 493dedcd89a..4fad0c09ce7 100644 --- a/app/assets/javascripts/ide/lib/editor.js +++ b/app/assets/javascripts/ide/lib/editor.js @@ -12,7 +12,7 @@ import { clearDomElement } from '~/editor/utils'; import { registerLanguages } from '../utils'; function setupThemes() { - themes.forEach(theme => { + themes.forEach((theme) => { monacoEditor.defineTheme(theme.name, theme.data); }); } @@ -108,7 +108,7 @@ export default class Editor { this.instance.updateOptions( editorOptions.reduce((acc, obj) => { - Object.keys(obj).forEach(key => { + Object.keys(obj).forEach((key) => { Object.assign(acc, { [key]: obj[key](model), }); @@ -177,7 +177,7 @@ export default class Editor { onPositionChange(cb) { if (!this.instance.onDidChangeCursorPosition) return; - this.disposable.add(this.instance.onDidChangeCursorPosition(e => cb(this.instance, e))); + this.disposable.add(this.instance.onDidChangeCursorPosition((e) => cb(this.instance, e))); } updateDiffView() { @@ -213,14 +213,14 @@ export default class Editor { addCommands() { const { store } = this; - const getKeyCode = key => { + const getKeyCode = (key) => { const monacoKeyMod = key.indexOf('KEY_') === 0; return monacoKeyMod ? KeyCode[key] : KeyMod[key]; }; - keymap.forEach(command => { - const keybindings = command.bindings.map(binding => { + keymap.forEach((command) => { + const keybindings = command.bindings.map((binding) => { const keys = binding.split('+'); // eslint-disable-next-line no-bitwise diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js index f182a1ec50e..9f2a9a8cf4a 100644 --- a/app/assets/javascripts/ide/lib/editor_options.js +++ b/app/assets/javascripts/ide/lib/editor_options.js @@ -31,7 +31,7 @@ export const defaultModelOptions = { export const editorOptions = [ { - readOnly: model => Boolean(model.file.file_lock), - quickSuggestions: model => !(model.language === 'markdown'), + readOnly: (model) => Boolean(model.file.file_lock), + quickSuggestions: (model) => !(model.language === 'markdown'), }, ]; diff --git a/app/assets/javascripts/ide/lib/editorconfig/parser.js b/app/assets/javascripts/ide/lib/editorconfig/parser.js index 1597e4a8bfa..2adc643a15b 100644 --- a/app/assets/javascripts/ide/lib/editorconfig/parser.js +++ b/app/assets/javascripts/ide/lib/editorconfig/parser.js @@ -2,7 +2,7 @@ import { parseString } from 'editorconfig/src/lib/ini'; import minimatch from 'minimatch'; import { getPathParents } from '../../utils'; -const dirname = path => path.replace(/\.editorconfig$/, ''); +const dirname = (path) => path.replace(/\.editorconfig$/, ''); function isRootConfig(config) { return config.some(([pattern, rules]) => !pattern && rules?.root === 'true'); @@ -44,11 +44,16 @@ function getRulesWithConfigs(filePath, configFiles = [], rules = {}) { export function getRulesWithTraversal(filePath, getFileContent) { const editorconfigPaths = [ - ...getPathParents(filePath).map(x => `${x}/.editorconfig`), + ...getPathParents(filePath).map((x) => `${x}/.editorconfig`), '.editorconfig', ]; return Promise.all( - editorconfigPaths.map(path => getFileContent(path).then(content => ({ path, content }))), - ).then(results => getRulesWithConfigs(filePath, results.filter(x => x.content))); + editorconfigPaths.map((path) => getFileContent(path).then((content) => ({ path, content }))), + ).then((results) => + getRulesWithConfigs( + filePath, + results.filter((x) => x.content), + ), + ); } diff --git a/app/assets/javascripts/ide/lib/editorconfig/rules_mapper.js b/app/assets/javascripts/ide/lib/editorconfig/rules_mapper.js index f9d5579511a..25ffa9a15be 100644 --- a/app/assets/javascripts/ide/lib/editorconfig/rules_mapper.js +++ b/app/assets/javascripts/ide/lib/editorconfig/rules_mapper.js @@ -1,23 +1,23 @@ import { isBoolean, isNumber } from 'lodash'; -const map = (key, validValues) => value => +const map = (key, validValues) => (value) => value in validValues ? { [key]: validValues[value] } : {}; -const bool = key => value => (isBoolean(value) ? { [key]: value } : {}); +const bool = (key) => (value) => (isBoolean(value) ? { [key]: value } : {}); -const int = (key, isValid) => value => +const int = (key, isValid) => (value) => isNumber(value) && isValid(value) ? { [key]: Math.trunc(value) } : {}; const rulesMapper = { indent_style: map('insertSpaces', { tab: false, space: true }), - indent_size: int('tabSize', n => n > 0), - tab_width: int('tabSize', n => n > 0), + indent_size: int('tabSize', (n) => n > 0), + tab_width: int('tabSize', (n) => n > 0), trim_trailing_whitespace: bool('trimTrailingWhitespace'), end_of_line: map('endOfLine', { crlf: 1, lf: 0 }), insert_final_newline: bool('insertFinalNewline'), }; -const parseValue = x => { +const parseValue = (x) => { let value = typeof x === 'string' ? x.toLowerCase() : x; if (/^[0-9.-]+$/.test(value)) value = Number(value); if (value === 'true') value = true; diff --git a/app/assets/javascripts/ide/lib/errors.js b/app/assets/javascripts/ide/lib/errors.js index e62d9d1e77f..f975034a872 100644 --- a/app/assets/javascripts/ide/lib/errors.js +++ b/app/assets/javascripts/ide/lib/errors.js @@ -6,17 +6,17 @@ const CODEOWNERS_REGEX = /Push.*protected branches.*CODEOWNERS/; const BRANCH_CHANGED_REGEX = /changed.*since.*start.*edit/; const BRANCH_ALREADY_EXISTS = /branch.*already.*exists/; -const createNewBranchAndCommit = store => +const createNewBranchAndCommit = (store) => store .dispatch('commit/updateCommitAction', consts.COMMIT_TO_NEW_BRANCH) .then(() => store.dispatch('commit/commitChanges')); -export const createUnexpectedCommitError = message => ({ +export const createUnexpectedCommitError = (message) => ({ title: __('Unexpected error'), messageHTML: escape(message) || __('Could not commit. An unexpected error occurred.'), }); -export const createCodeownersCommitError = message => ({ +export const createCodeownersCommitError = (message) => ({ title: __('CODEOWNERS rule violation'), messageHTML: escape(message), primaryAction: { @@ -25,7 +25,7 @@ export const createCodeownersCommitError = message => ({ }, }); -export const createBranchChangedCommitError = message => ({ +export const createBranchChangedCommitError = (message) => ({ title: __('Branch changed'), messageHTML: `${escape(message)}<br/><br/>${__('Would you like to create a new branch?')}`, primaryAction: { @@ -34,19 +34,19 @@ export const createBranchChangedCommitError = message => ({ }, }); -export const branchAlreadyExistsCommitError = message => ({ +export const branchAlreadyExistsCommitError = (message) => ({ title: __('Branch already exists'), messageHTML: `${escape(message)}<br/><br/>${__( 'Would you like to try auto-generating a branch name?', )}`, primaryAction: { text: __('Create new branch'), - callback: store => + callback: (store) => store.dispatch('commit/addSuffixToBranchName').then(() => createNewBranchAndCommit(store)), }, }); -export const parseCommitError = e => { +export const parseCommitError = (e) => { const { message } = e?.response?.data || {}; if (!message) { diff --git a/app/assets/javascripts/ide/lib/files.js b/app/assets/javascripts/ide/lib/files.js index 789e09fa8f2..3fdf012bbb2 100644 --- a/app/assets/javascripts/ide/lib/files.js +++ b/app/assets/javascripts/ide/lib/files.js @@ -1,6 +1,6 @@ import { decorateData, sortTree } from '../stores/utils'; -export const splitParent = path => { +export const splitParent = (path) => { const idx = path.lastIndexOf('/'); return { @@ -11,8 +11,20 @@ export const splitParent = path => { /** * Create file objects from a list of file paths. + * + * @param {Array} options.data Array of blob paths to parse and create a file tree from. + * @param {Boolean} options.tempFile Web IDE flag for whether this is a "new" file or not. + * @param {String} options.content Content to initialize the new blob with. + * @param {String} options.rawPath Raw path used for the new blob. + * @param {Object} options.blobData Extra values to initialize each blob with. */ -export const decorateFiles = ({ data, tempFile = false, content = '', rawPath = '' }) => { +export const decorateFiles = ({ + data, + tempFile = false, + content = '', + rawPath = '', + blobData = {}, +}) => { const treeList = []; const entries = {}; @@ -20,7 +32,7 @@ export const decorateFiles = ({ data, tempFile = false, content = '', rawPath = let file; let parentPath; - const insertParent = path => { + const insertParent = (path) => { if (!path) { return null; } else if (entries[path]) { @@ -55,7 +67,7 @@ export const decorateFiles = ({ data, tempFile = false, content = '', rawPath = return tree; }; - data.forEach(path => { + data.forEach((path) => { const { parent, name } = splitParent(path); const fileFolder = parent && insertParent(parent); @@ -73,6 +85,7 @@ export const decorateFiles = ({ data, tempFile = false, content = '', rawPath = content, rawPath, parentPath, + ...blobData, }); Object.assign(entries, { diff --git a/app/assets/javascripts/ide/lib/languages/hcl.js b/app/assets/javascripts/ide/lib/languages/hcl.js index 4539719b1f2..bbb2ca66f33 100644 --- a/app/assets/javascripts/ide/lib/languages/hcl.js +++ b/app/assets/javascripts/ide/lib/languages/hcl.js @@ -11,7 +11,11 @@ const conf = { lineComment: '//', blockComment: ['/*', '*/'], }, - brackets: [['{', '}'], ['[', ']'], ['(', ')']], + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ], autoClosingPairs: [ { open: '{', close: '}' }, { open: '[', close: ']' }, @@ -140,7 +144,7 @@ const language = { ], heredocBody: [ [ - /^([\w\-]+)$/, + /([\w\-]+)$/, { cases: { '$1==$S2': [ @@ -161,7 +165,11 @@ const language = { [/\/\/.*$/, 'comment'], [/#.*$/, 'comment'], ], - comment: [[/[^\/*]+/, 'comment'], [/\*\//, 'comment', '@pop'], [/[\/*]/, 'comment']], + comment: [ + [/[^\/*]+/, 'comment'], + [/\*\//, 'comment', '@pop'], + [/[\/*]/, 'comment'], + ], string: [ [/\$\{/, { token: 'delimiter', next: '@stringExpression' }], [/[^\\"\$]+/, 'string'], diff --git a/app/assets/javascripts/ide/lib/languages/vue.js b/app/assets/javascripts/ide/lib/languages/vue.js index b9ff5c5d776..f2f81307981 100644 --- a/app/assets/javascripts/ide/lib/languages/vue.js +++ b/app/assets/javascripts/ide/lib/languages/vue.js @@ -37,7 +37,13 @@ const conf = { blockComment: ['{{!--', '--}}'], }, - brackets: [['<!--', '-->'], ['<', '>'], ['{{', '}}'], ['{', '}'], ['(', ')']], + brackets: [ + ['<!--', '-->'], + ['<', '>'], + ['{{', '}}'], + ['{', '}'], + ['(', ')'], + ], autoClosingPairs: [ { open: '{', close: '}' }, diff --git a/app/assets/javascripts/ide/lib/mirror.js b/app/assets/javascripts/ide/lib/mirror.js index a516c28ad7a..6f9cfec9465 100644 --- a/app/assets/javascripts/ide/lib/mirror.js +++ b/app/assets/javascripts/ide/lib/mirror.js @@ -12,23 +12,23 @@ export const MSG_CONNECTION_ERROR = __('Could not connect to Web IDE file mirror const noop = () => {}; export const SERVICE_DELAY = 8000; -const cancellableWait = time => { +const cancellableWait = (time) => { let timeoutId = 0; const cancel = () => clearTimeout(timeoutId); - const promise = new Promise(resolve => { + const promise = new Promise((resolve) => { timeoutId = setTimeout(resolve, time); }); return [promise, cancel]; }; -const isErrorResponse = error => error && error.code !== 0; +const isErrorResponse = (error) => error && error.code !== 0; -const isErrorPayload = payload => payload && payload.status_code !== 200; +const isErrorPayload = (payload) => payload && payload.status_code !== 200; -const getErrorFromResponse = data => { +const getErrorFromResponse = (data) => { if (isErrorResponse(data.error)) { return { message: data.error.Message }; } else if (isErrorPayload(data.payload)) { @@ -38,9 +38,9 @@ const getErrorFromResponse = data => { return null; }; -const getFullPath = path => mergeUrlParams({ service: SERVICE_NAME }, getWebSocketUrl(path)); +const getFullPath = (path) => mergeUrlParams({ service: SERVICE_NAME }, getWebSocketUrl(path)); -const createWebSocket = fullPath => +const createWebSocket = (fullPath) => new Promise((resolve, reject) => { const socket = new WebSocket(fullPath, [PROTOCOL]); const resetCallbacks = () => { @@ -59,7 +59,7 @@ const createWebSocket = fullPath => }; }); -export const canConnect = ({ services = [] }) => services.some(name => name === SERVICE_NAME); +export const canConnect = ({ services = [] }) => services.some((name) => name === SERVICE_NAME); export const createMirror = () => { let socket = null; @@ -71,23 +71,23 @@ export const createMirror = () => { cancelHandler = noop; }; - const onCancelConnect = fn => { + const onCancelConnect = (fn) => { cancelHandler = fn; }; - const receiveMessage = ev => { + const receiveMessage = (ev) => { const handle = nextMessageHandler; nextMessageHandler = noop; handle(JSON.parse(ev.data)); }; - const onNextMessage = fn => { + const onNextMessage = (fn) => { nextMessageHandler = fn; }; const waitForNextMessage = () => new Promise((resolve, reject) => { - onNextMessage(data => { + onNextMessage((data) => { const err = getErrorFromResponse(data); if (err) { @@ -133,7 +133,7 @@ export const createMirror = () => { return wait .then(() => createWebSocket(fullPath)) - .then(newSocket => { + .then((newSocket) => { socket = newSocket; socket.onmessage = receiveMessage; }); diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index 70a6a6b423d..2264d63c737 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -4,9 +4,9 @@ import Api from '~/api'; import getUserPermissions from '../queries/getUserPermissions.query.graphql'; import { query } from './gql'; -const fetchApiProjectData = projectPath => Api.project(projectPath).then(({ data }) => data); +const fetchApiProjectData = (projectPath) => Api.project(projectPath).then(({ data }) => data); -const fetchGqlProjectData = projectPath => +const fetchGqlProjectData = (projectPath) => query({ query: getUserPermissions, variables: { projectPath }, @@ -27,9 +27,12 @@ export default { return Promise.resolve(file.raw); } + const options = file.binary ? { responseType: 'arraybuffer' } : {}; + return axios .get(file.rawPath, { - transformResponse: [f => f], + transformResponse: [(f) => f], + ...options, }) .then(({ data }) => data); }, @@ -51,7 +54,7 @@ export default { escapeFileUrl(filePath), ), { - transformResponse: [f => f], + transformResponse: [(f) => f], }, ) .then(({ data }) => data); diff --git a/app/assets/javascripts/ide/services/terminals.js b/app/assets/javascripts/ide/services/terminals.js index 17b4329037d..ea54733baa4 100644 --- a/app/assets/javascripts/ide/services/terminals.js +++ b/app/assets/javascripts/ide/services/terminals.js @@ -1,6 +1,6 @@ import axios from '~/lib/utils/axios_utils'; -export const baseUrl = projectPath => `/${projectPath}/ide_terminals`; +export const baseUrl = (projectPath) => `/${projectPath}/ide_terminals`; export const checkConfig = (projectPath, branch) => axios.post(`${baseUrl(projectPath)}/check_config`, { diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index 710256b6377..d62dfc35d15 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -20,7 +20,7 @@ export const redirectToUrl = (self, url) => visitUrl(url); export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); export const discardAllChanges = ({ state, commit, dispatch }) => { - state.changedFiles.forEach(file => dispatch('restoreOriginalFile', file.path)); + state.changedFiles.forEach((file) => dispatch('restoreOriginalFile', file.path)); commit(types.REMOVE_ALL_CHANGES_FILES); }; @@ -31,7 +31,7 @@ export const setResizingStatus = ({ commit }, resizing) => { export const createTempEntry = ( { state, commit, dispatch, getters }, - { name, type, content = '', rawPath = '', openFile = true, makeFileActive = true }, + { name, type, content = '', rawPath = '', openFile = true, makeFileActive = true, mimeType = '' }, ) => { const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name; @@ -56,6 +56,9 @@ export const createTempEntry = ( tempFile: true, content, rawPath, + blobData: { + mimeType, + }, }); const { file, parentPath } = data; @@ -103,7 +106,7 @@ export const stageAllChanges = ({ state, commit, dispatch, getters }) => { commit(types.SET_LAST_COMMIT_MSG, ''); - state.changedFiles.forEach(file => + state.changedFiles.forEach((file) => commit(types.STAGE_CHANGE, { path: file.path, diffInfo: getters.getDiffInfo(file.path) }), ); @@ -120,7 +123,7 @@ export const stageAllChanges = ({ state, commit, dispatch, getters }) => { export const unstageAllChanges = ({ state, commit, dispatch, getters }) => { const openFile = state.openFiles[0]; - state.stagedFiles.forEach(file => + state.stagedFiles.forEach((file) => commit(types.UNSTAGE_CHANGE, { path: file.path, diffInfo: getters.getDiffInfo(file.path) }), ); @@ -191,7 +194,7 @@ export const deleteEntry = ({ commit, dispatch, state }, path) => { if (entry.opened) dispatch('closeFile', entry); if (isTree) { - entry.tree.forEach(f => dispatch('deleteEntry', f.path)); + entry.tree.forEach((f) => dispatch('deleteEntry', f.path)); } commit(types.DELETE_ENTRY, path); @@ -218,7 +221,7 @@ export const renameEntry = ({ dispatch, commit, state, getters }, { path, name, commit(types.RENAME_ENTRY, { path, name, parentPath }); if (entry.type === 'tree') { - state.entries[newPath].tree.forEach(f => { + state.entries[newPath].tree.forEach((f) => { dispatch('renameEntry', { path: f.path, name: f.name, @@ -277,7 +280,7 @@ export const getBranchData = ({ commit, state }, { projectId, branchId, force = commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); resolve(data); }) - .catch(e => { + .catch((e) => { if (e.response.status === 404) { reject(e); } else { diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 8b43c7238fd..42668dec63a 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -10,22 +10,15 @@ import eventHub from '../../eventhub'; import service from '../../services'; import * as types from '../mutation_types'; import { setPageTitleForFile } from '../utils'; -import { viewerTypes, stageKeys } from '../../constants'; +import { viewerTypes, stageKeys, commitActionTypes } from '../../constants'; export const closeFile = ({ commit, state, dispatch, getters }, file) => { const { path } = file; - const indexOfClosedFile = state.openFiles.findIndex(f => f.key === file.key); + const indexOfClosedFile = state.openFiles.findIndex((f) => f.key === file.key); const fileWasActive = file.active; - if (file.pending) { - commit(types.REMOVE_PENDING_TAB, file); - } else { - commit(types.TOGGLE_FILE_OPEN, path); - commit(types.SET_FILE_ACTIVE, { path, active: false }); - } - - if (state.openFiles.length > 0 && fileWasActive) { - const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1; + if (state.openFiles.length > 1 && fileWasActive) { + const nextIndexToOpen = indexOfClosedFile === 0 ? 1 : indexOfClosedFile - 1; const nextFileToOpen = state.openFiles[nextIndexToOpen]; if (nextFileToOpen.pending) { @@ -35,14 +28,22 @@ export const closeFile = ({ commit, state, dispatch, getters }, file) => { keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged', }); } else { + dispatch('setFileActive', nextFileToOpen.path); dispatch('router/push', getters.getUrlForPath(nextFileToOpen.path), { root: true }); } - } else if (!state.openFiles.length) { + } else if (state.openFiles.length === 1) { dispatch('router/push', `/project/${state.currentProjectId}/tree/${state.currentBranchId}/`, { root: true, }); } + if (file.pending) { + commit(types.REMOVE_PENDING_TAB, file); + } else { + commit(types.TOGGLE_FILE_OPEN, path); + commit(types.SET_FILE_ACTIVE, { path, active: false }); + } + eventHub.$emit(`editor.update.model.dispose.${file.key}`); }; @@ -108,7 +109,7 @@ export const getFileData = ( .catch(() => { dispatch('setErrorMessage', { text: __('An error occurred while loading the file.'), - action: payload => + action: (payload) => dispatch('getFileData', payload).then(() => dispatch('setErrorMessage', null)), actionText: __('Please try again'), actionPayload: { path, makeFileActive }, @@ -125,13 +126,13 @@ export const setFileMrChange = ({ commit }, { file, mrChange }) => { export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) => { const file = state.entries[path]; - const stagedFile = state.stagedFiles.find(f => f.path === path); + const stagedFile = state.stagedFiles.find((f) => f.path === path); const fileDeletedAndReadded = getters.isFileDeletedAndReadded(path); commit(types.TOGGLE_LOADING, { entry: file, forceValue: true }); return service .getRawFileData(fileDeletedAndReadded ? stagedFile : file) - .then(raw => { + .then((raw) => { if (!(file.tempFile && !file.prevPath && !fileDeletedAndReadded)) commit(types.SET_FILE_RAW_DATA, { file, raw, fileDeletedAndReadded }); @@ -139,7 +140,7 @@ export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) = const baseSha = (getters.currentMergeRequest && getters.currentMergeRequest.baseCommitSha) || ''; - return service.getBaseRawFileData(file, state.currentProjectId, baseSha).then(baseRaw => { + return service.getBaseRawFileData(file, state.currentProjectId, baseSha).then((baseRaw) => { commit(types.SET_FILE_BASE_RAW_DATA, { file, baseRaw, @@ -149,10 +150,10 @@ export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) = } return raw; }) - .catch(e => { + .catch((e) => { dispatch('setErrorMessage', { text: __('An error occurred while loading the file content.'), - action: payload => + action: (payload) => dispatch('getRawFileData', payload).then(() => dispatch('setErrorMessage', null)), actionText: __('Please try again'), actionPayload: { path }, @@ -164,7 +165,7 @@ export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) = }); }; -export const changeFileContent = ({ commit, state, getters }, { path, content }) => { +export const changeFileContent = ({ commit, dispatch, state, getters }, { path, content }) => { const file = state.entries[path]; // It's possible for monaco to hit a race condition where it tries to update renamed files. @@ -178,13 +179,15 @@ export const changeFileContent = ({ commit, state, getters }, { path, content }) content, }); - const indexOfChangedFile = state.changedFiles.findIndex(f => f.path === path); + const indexOfChangedFile = state.changedFiles.findIndex((f) => f.path === path); if (file.changed && indexOfChangedFile === -1) { commit(types.STAGE_CHANGE, { path, diffInfo: getters.getDiffInfo(path) }); } else if (!file.changed && !file.tempFile && indexOfChangedFile !== -1) { commit(types.REMOVE_FILE_FROM_CHANGED, path); } + + dispatch('triggerFilesChange', { type: commitActionTypes.update, path }); }; export const restoreOriginalFile = ({ dispatch, state, commit }, path) => { @@ -225,7 +228,7 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) = .then(() => { dispatch('router/push', getters.getUrlForPath(file.path), { root: true }); }) - .catch(e => { + .catch((e) => { throw e; }); } @@ -275,7 +278,7 @@ export const unstageChange = ({ commit, dispatch, getters }, path) => { export const openPendingTab = ({ commit, dispatch, getters, state }, { file, keyPrefix }) => { if (getters.activeFile && getters.activeFile.key === `${keyPrefix}-${file.key}`) return false; - state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`)); + state.openFiles.forEach((f) => eventHub.$emit(`editor.update.model.dispose.${f.key}`)); commit(types.ADD_PENDING_TAB, { file, keyPrefix }); diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js index 547665b49c6..8215cba7ccf 100644 --- a/app/assets/javascripts/ide/stores/actions/merge_request.js +++ b/app/assets/javascripts/ide/stores/actions/merge_request.js @@ -33,7 +33,7 @@ export const getMergeRequestsForBranch = ( commit(types.SET_CURRENT_MERGE_REQUEST, `${currentMR.iid}`); } }) - .catch(e => { + .catch((e) => { flash( __(`Error fetching merge requests for ${branchId}`), 'alert', @@ -66,7 +66,7 @@ export const getMergeRequestData = ( .catch(() => { dispatch('setErrorMessage', { text: __('An error occurred while loading the merge request.'), - action: payload => + action: (payload) => dispatch('getMergeRequestData', payload).then(() => dispatch('setErrorMessage', null), ), @@ -99,7 +99,7 @@ export const getMergeRequestChanges = ( .catch(() => { dispatch('setErrorMessage', { text: __('An error occurred while loading the merge request changes.'), - action: payload => + action: (payload) => dispatch('getMergeRequestChanges', payload).then(() => dispatch('setErrorMessage', null), ), @@ -121,8 +121,8 @@ export const getMergeRequestVersions = ( if (!state.projects[projectId].mergeRequests[mergeRequestId].versions.length || force) { service .getProjectMergeRequestVersions(targetProjectId || projectId, mergeRequestId) - .then(res => res.data) - .then(data => { + .then((res) => res.data) + .then((data) => { commit(types.SET_MERGE_REQUEST_VERSIONS, { projectPath: projectId, mergeRequestId, @@ -133,7 +133,7 @@ export const getMergeRequestVersions = ( .catch(() => { dispatch('setErrorMessage', { text: __('An error occurred while loading the merge request version data.'), - action: payload => + action: (payload) => dispatch('getMergeRequestVersions', payload).then(() => dispatch('setErrorMessage', null), ), @@ -156,7 +156,7 @@ export const openMergeRequest = ( targetProjectId, mergeRequestId, }) - .then(mr => { + .then((mr) => { dispatch('setCurrentBranchId', mr.source_branch); return dispatch('getBranchData', { @@ -186,7 +186,7 @@ export const openMergeRequest = ( mergeRequestId, }), ) - .then(mrChanges => { + .then((mrChanges) => { if (mrChanges.changes.length) { dispatch('updateActivityBarView', leftSidebarViews.review.name); } @@ -210,7 +210,7 @@ export const openMergeRequest = ( } }); }) - .catch(e => { + .catch((e) => { flash(__('Error while loading the merge request. Please try again.')); throw e; }); diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 51e9bf6a84c..27f6848f1d6 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -11,8 +11,8 @@ export const getProjectData = ({ commit, state }, { namespace, projectId, force commit(types.TOGGLE_LOADING, { entry: state }); service .getProjectData(namespace, projectId) - .then(res => res.data) - .then(data => { + .then((res) => res.data) + .then((data) => { commit(types.TOGGLE_LOADING, { entry: state }); commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data }); commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); @@ -44,8 +44,9 @@ export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) commit: data.commit, }); }) - .catch(() => { + .catch((e) => { flash(__('Error loading last commit.'), 'alert', document, null, false, true); + throw e; }); export const createNewBranchFromDefault = ({ state, dispatch, getters }, branch) => @@ -61,7 +62,7 @@ export const createNewBranchFromDefault = ({ state, dispatch, getters }, branch) .catch(() => { dispatch('setErrorMessage', { text: __('An error occurred creating the new branch.'), - action: payload => dispatch('createNewBranchFromDefault', payload), + action: (payload) => dispatch('createNewBranchFromDefault', payload), actionText: __('Please try again'), actionPayload: branch, }); @@ -76,7 +77,7 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => { }, false, ), - action: payload => dispatch('createNewBranchFromDefault', payload), + action: (payload) => dispatch('createNewBranchFromDefault', payload), actionText: __('Create branch'), actionPayload: branchId, }); @@ -102,7 +103,7 @@ export const loadFile = ({ dispatch, state }, { basePath }) => { if (basePath) { const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath; const treeEntryKey = Object.keys(state.entries).find( - key => key === path && !state.entries[key].pending, + (key) => key === path && !state.entries[key].pending, ); const treeEntry = state.entries[treeEntryKey]; @@ -144,7 +145,7 @@ export const loadBranch = ({ dispatch, getters, state }, { projectId, branchId } ref: branch.commit.id, }); }) - .catch(err => { + .catch((err) => { dispatch('showBranchNotFoundError', branchId); throw err; }); diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js index 23a5e26bc1c..150dfcb2726 100644 --- a/app/assets/javascripts/ide/stores/actions/tree.js +++ b/app/assets/javascripts/ide/stores/actions/tree.js @@ -87,10 +87,10 @@ export const getFiles = ({ state, commit, dispatch }, payload = {}) => { resolve(); }) - .catch(e => { + .catch((e) => { dispatch('setErrorMessage', { text: __('An error occurred while loading all the files.'), - action: actionPayload => + action: (actionPayload) => dispatch('getFiles', actionPayload).then(() => dispatch('setErrorMessage', null)), actionText: __('Please try again'), actionPayload: { projectId, branchId }, diff --git a/app/assets/javascripts/ide/stores/extend.js b/app/assets/javascripts/ide/stores/extend.js index 1c1636cf6ca..b2777ec89ff 100644 --- a/app/assets/javascripts/ide/stores/extend.js +++ b/app/assets/javascripts/ide/stores/extend.js @@ -8,7 +8,7 @@ const plugins = () => [ export default (store, el) => { // plugins is actually an array of plugin factories, so we have to create first then call - plugins().forEach(plugin => plugin(el)(store)); + plugins().forEach((plugin) => plugin(el)(store)); return store; }; diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index 500ce9f32d5..59e8d37a92a 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -9,19 +9,19 @@ import { import { addNumericSuffix } from '~/ide/utils'; import Api from '~/api'; -export const activeFile = state => state.openFiles.find(file => file.active) || null; +export const activeFile = (state) => state.openFiles.find((file) => file.active) || null; -export const addedFiles = state => state.changedFiles.filter(f => f.tempFile); +export const addedFiles = (state) => state.changedFiles.filter((f) => f.tempFile); -export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile); +export const modifiedFiles = (state) => state.changedFiles.filter((f) => !f.tempFile); -export const projectsWithTrees = state => - Object.keys(state.projects).map(projectId => { +export const projectsWithTrees = (state) => + Object.keys(state.projects).map((projectId) => { const project = state.projects[projectId]; return { ...project, - branches: Object.keys(project.branches).map(branchId => { + branches: Object.keys(project.branches).map((branchId) => { const branch = project.branches[branchId]; return { @@ -32,7 +32,7 @@ export const projectsWithTrees = state => }; }); -export const currentMergeRequest = state => { +export const currentMergeRequest = (state) => { if ( state.projects[state.currentProjectId] && state.projects[state.currentProjectId].mergeRequests @@ -42,19 +42,19 @@ export const currentMergeRequest = state => { return null; }; -export const findProject = state => projectId => state.projects[projectId]; +export const findProject = (state) => (projectId) => state.projects[projectId]; export const currentProject = (state, getters) => getters.findProject(state.currentProjectId); -export const emptyRepo = state => +export const emptyRepo = (state) => state.projects[state.currentProjectId] && state.projects[state.currentProjectId].empty_repo; -export const currentTree = state => +export const currentTree = (state) => state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; -export const hasMergeRequest = state => Boolean(state.currentMergeRequestId); +export const hasMergeRequest = (state) => Boolean(state.currentMergeRequestId); -export const allBlobs = state => +export const allBlobs = (state) => Object.keys(state.entries) .reduce((acc, key) => { const entry = state.entries[key]; @@ -67,35 +67,35 @@ export const allBlobs = state => }, []) .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt); -export const getChangedFile = state => path => state.changedFiles.find(f => f.path === path); -export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path); -export const getOpenFile = state => path => state.openFiles.find(f => f.path === path); +export const getChangedFile = (state) => (path) => state.changedFiles.find((f) => f.path === path); +export const getStagedFile = (state) => (path) => state.stagedFiles.find((f) => f.path === path); +export const getOpenFile = (state) => (path) => state.openFiles.find((f) => f.path === path); -export const lastOpenedFile = state => +export const lastOpenedFile = (state) => [...state.changedFiles, ...state.stagedFiles].sort((a, b) => b.lastOpenedAt - a.lastOpenedAt)[0]; -export const isEditModeActive = state => state.currentActivityView === leftSidebarViews.edit.name; -export const isCommitModeActive = state => +export const isEditModeActive = (state) => state.currentActivityView === leftSidebarViews.edit.name; +export const isCommitModeActive = (state) => state.currentActivityView === leftSidebarViews.commit.name; -export const isReviewModeActive = state => +export const isReviewModeActive = (state) => state.currentActivityView === leftSidebarViews.review.name; -export const someUncommittedChanges = state => +export const someUncommittedChanges = (state) => Boolean(state.changedFiles.length || state.stagedFiles.length); -export const getChangesInFolder = state => path => { - const changedFilesCount = state.changedFiles.filter(f => filePathMatches(f.path, path)).length; +export const getChangesInFolder = (state) => (path) => { + const changedFilesCount = state.changedFiles.filter((f) => filePathMatches(f.path, path)).length; const stagedFilesCount = state.stagedFiles.filter( - f => filePathMatches(f.path, path) && !getChangedFile(state)(f.path), + (f) => filePathMatches(f.path, path) && !getChangedFile(state)(f.path), ).length; return changedFilesCount + stagedFilesCount; }; -export const getUnstagedFilesCountForPath = state => path => +export const getUnstagedFilesCountForPath = (state) => (path) => getChangesCountForFiles(state.changedFiles, path); -export const getStagedFilesCountForPath = state => path => +export const getStagedFilesCountForPath = (state) => (path) => getChangesCountForFiles(state.stagedFiles, path); export const lastCommit = (state, getters) => { @@ -115,7 +115,7 @@ export const currentBranch = (state, getters) => export const branchName = (_state, getters) => getters.currentBranch && getters.currentBranch.name; -export const packageJson = state => state.entries[packageJsonPath]; +export const packageJson = (state) => state.entries[packageJsonPath]; export const isOnDefaultBranch = (_state, getters) => getters.currentProject && getters.currentProject.default_branch === getters.branchName; @@ -124,14 +124,14 @@ export const canPushToBranch = (_state, getters) => { return Boolean(getters.currentBranch ? getters.currentBranch.can_push : getters.canPushCode); }; -export const isFileDeletedAndReadded = (state, getters) => path => { +export const isFileDeletedAndReadded = (state, getters) => (path) => { const stagedFile = getters.getStagedFile(path); const file = state.entries[path]; return Boolean(stagedFile && stagedFile.deleted && file.tempFile); }; // checks if any diff exists in the staged or unstaged changes for this path -export const getDiffInfo = (state, getters) => path => { +export const getDiffInfo = (state, getters) => (path) => { const stagedFile = getters.getStagedFile(path); const file = state.entries[path]; const renamed = file.prevPath ? file.path !== file.prevPath : false; @@ -149,7 +149,7 @@ export const getDiffInfo = (state, getters) => path => { }; }; -export const findProjectPermissions = (state, getters) => projectId => +export const findProjectPermissions = (state, getters) => (projectId) => getters.findProject(projectId)?.userPermissions || {}; export const canReadMergeRequests = (state, getters) => @@ -161,10 +161,10 @@ export const canCreateMergeRequests = (state, getters) => export const canPushCode = (state, getters) => Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_PUSH_CODE]); -export const entryExists = state => path => +export const entryExists = (state) => (path) => Boolean(state.entries[path] && !state.entries[path].deleted); -export const getAvailableFileName = (state, getters) => path => { +export const getAvailableFileName = (state, getters) => (path) => { let newPath = path; while (getters.entryExists(newPath)) { @@ -174,10 +174,10 @@ export const getAvailableFileName = (state, getters) => path => { return newPath; }; -export const getUrlForPath = state => path => +export const getUrlForPath = (state) => (path) => `/project/${state.currentProjectId}/tree/${state.currentBranchId}/-/${path}/`; -export const getJsonSchemaForPath = (state, getters) => path => { +export const getJsonSchemaForPath = (state, getters) => (path) => { const [namespace, ...project] = state.currentProjectId.split('/'); return { uri: diff --git a/app/assets/javascripts/ide/stores/modules/branches/actions.js b/app/assets/javascripts/ide/stores/modules/branches/actions.js index c46289f77e2..74a4cd9848b 100644 --- a/app/assets/javascripts/ide/stores/modules/branches/actions.js +++ b/app/assets/javascripts/ide/stores/modules/branches/actions.js @@ -8,7 +8,7 @@ export const receiveBranchesError = ({ commit, dispatch }, { search }) => { 'setErrorMessage', { text: __('Error loading branches.'), - action: payload => + action: (payload) => dispatch('fetchBranches', payload).then(() => dispatch('setErrorMessage', null, { root: true }), ), diff --git a/app/assets/javascripts/ide/stores/modules/branches/mutations.js b/app/assets/javascripts/ide/stores/modules/branches/mutations.js index 0a455f4500f..3883e1cc905 100644 --- a/app/assets/javascripts/ide/stores/modules/branches/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/branches/mutations.js @@ -9,7 +9,7 @@ export default { }, [types.RECEIVE_BRANCHES_SUCCESS](state, data) { state.isLoading = false; - state.branches = data.map(branch => ({ + state.branches = data.map((branch) => ({ name: branch.name, committedDate: branch.commit.committed_date, })); diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index e0d2028d2e1..29b9a8a9521 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -78,8 +78,8 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState, rootGetter { root: true }, ); - rootState.stagedFiles.forEach(file => { - const changedFile = rootState.changedFiles.find(f => f.path === file.path); + rootState.stagedFiles.forEach((file) => { + const changedFile = rootState.changedFiles.find((f) => f.path === file.path); commit( rootTypes.UPDATE_FILE_AFTER_COMMIT, @@ -133,7 +133,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo return service.commit(rootState.currentProjectId, payload); }) - .catch(e => { + .catch((e) => { commit(types.UPDATE_LOADING, false); commit(types.SET_ERROR, parseCommitError(e)); @@ -193,37 +193,36 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo }, { root: true }, ) - .then(changeViewer => { + .then((changeViewer) => { if (changeViewer) { dispatch('updateViewer', 'diff', { root: true }); } }) - .catch(e => { + .catch((e) => { throw e; }); } else { dispatch('updateActivityBarView', leftSidebarViews.edit.name, { root: true }); dispatch('updateViewer', 'editor', { root: true }); - - if (rootGetters.activeFile) { - dispatch( - 'router/push', - `/project/${rootState.currentProjectId}/blob/${branchName}/-/${rootGetters.activeFile.path}`, - { root: true }, - ); - } } }) .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH)) - .then(() => - dispatch( + .then(() => { + if (newBranch) { + const path = rootGetters.activeFile ? rootGetters.activeFile.path : ''; + + return dispatch( + 'router/push', + `/project/${rootState.currentProjectId}/blob/${branchName}/-/${path}`, + { root: true }, + ); + } + + return dispatch( 'refreshLastCommitData', - { - projectId: rootState.currentProjectId, - branchId: rootState.currentBranchId, - }, + { projectId: rootState.currentProjectId, branchId: branchName }, { root: true }, - ), - ); + ); + }); }); }; diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js index 416ca88d6c9..2301cf23f9f 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/getters.js +++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js @@ -11,7 +11,7 @@ const createTranslatedTextForFiles = (files, text) => { }); }; -export const discardDraftButtonDisabled = state => +export const discardDraftButtonDisabled = (state) => state.commitMessage === '' || state.submitCommitLoading; // Note: If changing the structure of the placeholder branch name, please also @@ -37,18 +37,18 @@ export const preBuiltCommitMessage = (state, _, rootState) => { if (state.commitMessage) return state.commitMessage; const files = rootState.stagedFiles.length ? rootState.stagedFiles : rootState.changedFiles; - const modifiedFiles = files.filter(f => !f.deleted); - const deletedFiles = files.filter(f => f.deleted); + const modifiedFiles = files.filter((f) => !f.deleted); + const deletedFiles = files.filter((f) => f.deleted); return [ createTranslatedTextForFiles(modifiedFiles, __('Update')), createTranslatedTextForFiles(deletedFiles, __('Deleted')), ] - .filter(t => t) + .filter((t) => t) .join('\n'); }; -export const isCreatingNewBranch = state => state.commitAction === consts.COMMIT_TO_NEW_BRANCH; +export const isCreatingNewBranch = (state) => state.commitAction === consts.COMMIT_TO_NEW_BRANCH; export const shouldHideNewMrOption = (_state, getters, _rootState, rootGetters) => !getters.isCreatingNewBranch && diff --git a/app/assets/javascripts/ide/stores/modules/editor/setup.js b/app/assets/javascripts/ide/stores/modules/editor/setup.js index c5a613c6baa..9f3163aa6f5 100644 --- a/app/assets/javascripts/ide/stores/modules/editor/setup.js +++ b/app/assets/javascripts/ide/stores/modules/editor/setup.js @@ -1,18 +1,23 @@ import eventHub from '~/ide/eventhub'; import { commitActionTypes } from '~/ide/constants'; -const removeUnusedFileEditors = store => { +const removeUnusedFileEditors = (store) => { Object.keys(store.state.editor.fileEditors) - .filter(path => !store.state.entries[path]) - .forEach(path => store.dispatch('editor/removeFileEditor', path)); + .filter((path) => !store.state.entries[path]) + .forEach((path) => store.dispatch('editor/removeFileEditor', path)); }; -export const setupFileEditorsSync = store => { +export const setupFileEditorsSync = (store) => { eventHub.$on('ide.files.change', ({ type, ...payload } = {}) => { + // Do nothing on file update because the file tree itself hasn't changed. + if (type === commitActionTypes.update) { + return; + } + if (type === commitActionTypes.move) { store.dispatch('editor/renameFileEditor', payload); } else { - // The files have changed, but the specific change is not known. + // The file tree has changed, but the specific change is not known. removeUnusedFileEditors(store); } }); diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js index 6b2c929cd44..6800f824da0 100644 --- a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js +++ b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js @@ -68,7 +68,7 @@ export const receiveTemplateError = ({ dispatch }, template) => { 'setErrorMessage', { text: __('Error loading template.'), - action: payload => + action: (payload) => dispatch('fetchTemplateTypes', payload).then(() => dispatch('setErrorMessage', null, { root: true }), ), diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js index 4a407aea557..0613fe9b12b 100644 --- a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js +++ b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js @@ -24,6 +24,6 @@ export const templateTypes = () => [ }, ]; -export const showFileTemplatesBar = (_, getters, rootState) => name => - getters.templateTypes.find(t => t.name === name) && +export const showFileTemplatesBar = (_, getters, rootState) => (name) => + getters.templateTypes.find((t) => t.name === name) && rootState.currentActivityView === leftSidebarViews.edit.name; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js index 6a1a0de033e..299f7a883d2 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js @@ -9,7 +9,7 @@ export const receiveMergeRequestsError = ({ commit, dispatch }, { type, search } 'setErrorMessage', { text: __('Error loading merge requests.'), - action: payload => + action: (payload) => dispatch('fetchMergeRequests', payload).then(() => dispatch('setErrorMessage', null, { root: true }), ), diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js index 7576b2477d1..eae64ad80c3 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js @@ -9,7 +9,7 @@ export default { }, [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, data) { state.isLoading = false; - state.mergeRequests = data.map(mergeRequest => ({ + state.mergeRequests = data.map((mergeRequest) => ({ id: mergeRequest.id, iid: mergeRequest.iid, title: mergeRequest.title, diff --git a/app/assets/javascripts/ide/stores/modules/pane/getters.js b/app/assets/javascripts/ide/stores/modules/pane/getters.js index ce597329df1..66d23c8ebdc 100644 --- a/app/assets/javascripts/ide/stores/modules/pane/getters.js +++ b/app/assets/javascripts/ide/stores/modules/pane/getters.js @@ -1,2 +1,2 @@ -export const isAliveView = state => view => +export const isAliveView = (state) => (view) => state.keepAliveViews[view] || (state.isOpen && state.currentView === view); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js index 99bd08ee876..2c2034d76d0 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js @@ -47,7 +47,7 @@ export const receiveLatestPipelineSuccess = ({ rootGetters, commit }, { pipeline if (pipelines && pipelines.length) { const lastCommitHash = rootGetters.lastCommit && rootGetters.lastCommit.id; - lastCommitPipeline = pipelines.find(pipeline => pipeline.commit.id === lastCommitHash); + lastCommitPipeline = pipelines.find((pipeline) => pipeline.commit.id === lastCommitHash); } commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, lastCommitPipeline); @@ -63,7 +63,7 @@ export const fetchLatestPipeline = ({ dispatch, rootGetters }) => { method: 'lastCommitPipelines', data: { getters: rootGetters }, successCallback: ({ data }) => dispatch('receiveLatestPipelineSuccess', data), - errorCallback: err => dispatch('receiveLatestPipelineError', err), + errorCallback: (err) => dispatch('receiveLatestPipelineError', err), }); if (!Visibility.hidden()) { @@ -85,7 +85,7 @@ export const receiveJobsError = ({ commit, dispatch }, stage) => { 'setErrorMessage', { text: __('An error occurred while loading the pipelines jobs.'), - action: payload => + action: (payload) => dispatch('fetchJobs', payload).then(() => dispatch('setErrorMessage', null, { root: true }), ), diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js index eb3cc027494..051159a0fd5 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js @@ -1,22 +1,23 @@ import { states } from './constants'; -export const hasLatestPipeline = state => !state.isLoadingPipeline && Boolean(state.latestPipeline); +export const hasLatestPipeline = (state) => + !state.isLoadingPipeline && Boolean(state.latestPipeline); -export const pipelineFailed = state => +export const pipelineFailed = (state) => state.latestPipeline && state.latestPipeline.details.status.text === states.failed; -export const failedStages = state => +export const failedStages = (state) => state.stages - .filter(stage => stage.status.text.toLowerCase() === states.failed) - .map(stage => ({ + .filter((stage) => stage.status.text.toLowerCase() === states.failed) + .map((stage) => ({ ...stage, - jobs: stage.jobs.filter(job => job.status.text.toLowerCase() === states.failed), + jobs: stage.jobs.filter((job) => job.status.text.toLowerCase() === states.failed), })); -export const failedJobsCount = state => +export const failedJobsCount = (state) => state.stages.reduce( - (acc, stage) => acc + stage.jobs.filter(j => j.status.text === states.failed).length, + (acc, stage) => acc + stage.jobs.filter((j) => j.status.text === states.failed).length, 0, ); -export const jobsCount = state => state.stages.reduce((acc, stage) => acc + stage.jobs.length, 0); +export const jobsCount = (state) => state.stages.reduce((acc, stage) => acc + stage.jobs.length, 0); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js index 3a3cb4a7cb2..09006df7e94 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js @@ -23,7 +23,7 @@ export default { yamlError: pipeline.yaml_errors, }; state.stages = pipeline.details.stages.map((stage, i) => { - const foundStage = state.stages.find(s => s.id === i); + const foundStage = state.stages.find((s) => s.id === i); return { id: i, dropdownPath: stage.dropdown_path, @@ -39,26 +39,26 @@ export default { } }, [types.REQUEST_JOBS](state, id) { - state.stages = state.stages.map(stage => ({ + state.stages = state.stages.map((stage) => ({ ...stage, isLoading: stage.id === id ? true : stage.isLoading, })); }, [types.RECEIVE_JOBS_ERROR](state, id) { - state.stages = state.stages.map(stage => ({ + state.stages = state.stages.map((stage) => ({ ...stage, isLoading: stage.id === id ? false : stage.isLoading, })); }, [types.RECEIVE_JOBS_SUCCESS](state, { id, data }) { - state.stages = state.stages.map(stage => ({ + state.stages = state.stages.map((stage) => ({ ...stage, isLoading: stage.id === id ? false : stage.isLoading, jobs: stage.id === id ? data.latest_statuses.map(normalizeJob) : stage.jobs, })); }, [types.TOGGLE_STAGE_COLLAPSE](state, id) { - state.stages = state.stages.map(stage => ({ + state.stages = state.stages.map((stage) => ({ ...stage, isCollapsed: stage.id === id ? !stage.isCollapsed : stage.isCollapsed, })); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js index 95716e0a0c6..ded00196ab7 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js @@ -1,4 +1,4 @@ -export const normalizeJob = job => ({ +export const normalizeJob = (job) => ({ id: job.id, name: job.name, status: job.status, diff --git a/app/assets/javascripts/ide/stores/modules/terminal/actions/checks.js b/app/assets/javascripts/ide/stores/modules/terminal/actions/checks.js index 43b6650b241..b2c1ddd877c 100644 --- a/app/assets/javascripts/ide/stores/modules/terminal/actions/checks.js +++ b/app/assets/javascripts/ide/stores/modules/terminal/actions/checks.js @@ -36,7 +36,7 @@ export const fetchConfigCheck = ({ dispatch, rootState, rootGetters }) => { .then(() => { dispatch('receiveConfigCheckSuccess'); }) - .catch(e => { + .catch((e) => { dispatch('receiveConfigCheckError', e); }); }; @@ -92,7 +92,7 @@ export const fetchRunnersCheck = ({ dispatch, rootGetters }, options = {}) => { .then(({ data }) => { dispatch('receiveRunnersCheckSuccess', data); }) - .catch(e => { + .catch((e) => { dispatch('receiveRunnersCheckError', e); }); }; diff --git a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js index f20f7fc9cd6..aa460859b4c 100644 --- a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js +++ b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js @@ -45,7 +45,7 @@ export const startSession = ({ state, dispatch, rootGetters, rootState }) => { .then(({ data }) => { dispatch('receiveStartSessionSuccess', data); }) - .catch(error => { + .catch((error) => { dispatch('receiveStartSessionError', error); }); }; @@ -73,7 +73,7 @@ export const stopSession = ({ state, dispatch }) => { .then(() => { dispatch('receiveStopSessionSuccess'); }) - .catch(err => { + .catch((err) => { dispatch('receiveStopSessionError', err); }); }; @@ -103,7 +103,7 @@ export const restartSession = ({ state, dispatch, rootState }) => { .then(({ data }) => { dispatch('receiveStartSessionSuccess', data); }) - .catch(error => { + .catch((error) => { const responseStatus = error.response && error.response.status; // We may have removed the build, in this case we'll just create a new session if ( diff --git a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js index d715d555aa9..3ab1817e662 100644 --- a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js +++ b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js @@ -58,7 +58,7 @@ export const fetchSessionStatus = ({ dispatch, state }) => { .then(({ data }) => { dispatch('receiveSessionStatusSuccess', data); }) - .catch(error => { + .catch((error) => { dispatch('receiveSessionStatusError', error); }); }; diff --git a/app/assets/javascripts/ide/stores/modules/terminal/getters.js b/app/assets/javascripts/ide/stores/modules/terminal/getters.js index b29d391845d..fb9a1a2fa39 100644 --- a/app/assets/javascripts/ide/stores/modules/terminal/getters.js +++ b/app/assets/javascripts/ide/stores/modules/terminal/getters.js @@ -1,11 +1,11 @@ -export const allCheck = state => { +export const allCheck = (state) => { const checks = Object.values(state.checks); - if (checks.some(check => check.isLoading)) { + if (checks.some((check) => check.isLoading)) { return { isLoading: true }; } - const invalidCheck = checks.find(check => !check.isValid); + const invalidCheck = checks.find((check) => !check.isValid); const isValid = !invalidCheck; const message = !invalidCheck ? '' : invalidCheck.message; diff --git a/app/assets/javascripts/ide/stores/modules/terminal/messages.js b/app/assets/javascripts/ide/stores/modules/terminal/messages.js index bf35ce0f0bc..967ba80cd2c 100644 --- a/app/assets/javascripts/ide/stores/modules/terminal/messages.js +++ b/app/assets/javascripts/ide/stores/modules/terminal/messages.js @@ -46,7 +46,7 @@ export const configCheckError = (status, helpUrl) => { return UNEXPECTED_ERROR_CONFIG; }; -export const runnersCheckEmpty = helpUrl => +export const runnersCheckEmpty = (helpUrl) => sprintf( EMPTY_RUNNERS, { diff --git a/app/assets/javascripts/ide/stores/modules/terminal/utils.js b/app/assets/javascripts/ide/stores/modules/terminal/utils.js index c30136b5277..1f4bca9f50a 100644 --- a/app/assets/javascripts/ide/stores/modules/terminal/utils.js +++ b/app/assets/javascripts/ide/stores/modules/terminal/utils.js @@ -1,5 +1,5 @@ import { STARTING, PENDING, RUNNING } from './constants'; -export const isStartingStatus = status => status === STARTING || status === PENDING; -export const isRunningStatus = status => status === RUNNING; -export const isEndingStatus = status => !isStartingStatus(status) && !isRunningStatus(status); +export const isStartingStatus = (status) => status === STARTING || status === PENDING; +export const isRunningStatus = (status) => status === RUNNING; +export const isEndingStatus = (status) => !isStartingStatus(status) && !isRunningStatus(status); diff --git a/app/assets/javascripts/ide/stores/modules/terminal_sync/actions.js b/app/assets/javascripts/ide/stores/modules/terminal_sync/actions.js index 2fee6b4e974..006800f58c2 100644 --- a/app/assets/javascripts/ide/stores/modules/terminal_sync/actions.js +++ b/app/assets/javascripts/ide/stores/modules/terminal_sync/actions.js @@ -9,7 +9,7 @@ export const upload = ({ rootState, commit }) => { .then(() => { commit(types.SET_SUCCESS); }) - .catch(err => { + .catch((err) => { commit(types.SET_ERROR, err); }); }; @@ -34,7 +34,7 @@ export const start = ({ rootState, commit }) => { .then(() => { commit(types.SET_SUCCESS); }) - .catch(err => { + .catch((err) => { commit(types.SET_ERROR, err); throw err; }); diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 460d3ced381..6ed6798a5b6 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -61,7 +61,7 @@ export default { }); } else { const tree = entry.tree.filter( - f => foundEntry.tree.find(e => e.path === f.path) === undefined, + (f) => foundEntry.tree.find((e) => e.path === f.path) === undefined, ); Object.assign(foundEntry, { tree: sortTree(foundEntry.tree.concat(tree)), @@ -72,7 +72,7 @@ export default { }, []); const currentTree = state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; - const foundEntry = currentTree.tree.find(e => e.path === data.treeList[0].path); + const foundEntry = currentTree.tree.find((e) => e.path === data.treeList[0].path); if (!foundEntry) { Object.assign(currentTree, { @@ -125,7 +125,7 @@ export default { }); }, [types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) { - const changedFile = state.changedFiles.find(f => f.path === file.path); + const changedFile = state.changedFiles.find((f) => f.path === file.path); const { prevPath } = file; Object.assign(state.entries[file.path], { @@ -172,7 +172,7 @@ export default { entry.deleted = true; if (parent) { - parent.tree = parent.tree.filter(f => f.path !== entry.path); + parent.tree = parent.tree.filter((f) => f.path !== entry.path); } if (entry.type === 'blob') { @@ -181,8 +181,8 @@ export default { // changed and staged. Otherwise, we'd need to somehow evaluate the difference between // changed and HEAD. // https://gitlab.com/gitlab-org/create-stage/-/issues/12669 - state.changedFiles = state.changedFiles.filter(f => f.path !== path); - state.stagedFiles = state.stagedFiles.filter(f => f.path !== path); + state.changedFiles = state.changedFiles.filter((f) => f.path !== path); + state.stagedFiles = state.stagedFiles.filter((f) => f.path !== path); } else { state.changedFiles = state.changedFiles.concat(entry); } diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index 61a55d45128..4446971d5d6 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -12,7 +12,7 @@ export default { if (active && !state.entries[path].pending) { Object.assign(state, { - openFiles: state.openFiles.map(f => + openFiles: state.openFiles.map((f) => Object.assign(f, { active: f.pending ? false : f.active }), ), }); @@ -28,21 +28,21 @@ export default { if (entry.opened) { Object.assign(state, { - openFiles: state.openFiles.filter(f => f.path !== path).concat(state.entries[path]), + openFiles: state.openFiles.filter((f) => f.path !== path).concat(state.entries[path]), }); } else { Object.assign(state, { - openFiles: state.openFiles.filter(f => f.key !== entry.key), + openFiles: state.openFiles.filter((f) => f.key !== entry.key), }); } }, [types.SET_FILE_DATA](state, { data, file }) { const stateEntry = state.entries[file.path]; - const stagedFile = state.stagedFiles.find(f => f.path === file.path); - const openFile = state.openFiles.find(f => f.path === file.path); - const changedFile = state.changedFiles.find(f => f.path === file.path); + const stagedFile = state.stagedFiles.find((f) => f.path === file.path); + const openFile = state.openFiles.find((f) => f.path === file.path); + const changedFile = state.changedFiles.find((f) => f.path === file.path); - [stateEntry, stagedFile, openFile, changedFile].forEach(f => { + [stateEntry, stagedFile, openFile, changedFile].forEach((f) => { if (f) { Object.assign( f, @@ -57,10 +57,10 @@ export default { }, [types.SET_FILE_RAW_DATA](state, { file, raw, fileDeletedAndReadded = false }) { const openPendingFile = state.openFiles.find( - f => + (f) => f.path === file.path && f.pending && !(f.tempFile && !f.prevPath && !fileDeletedAndReadded), ); - const stagedFile = state.stagedFiles.find(f => f.path === file.path); + const stagedFile = state.stagedFiles.find((f) => f.path === file.path); if (file.tempFile && file.content === '' && !fileDeletedAndReadded) { Object.assign(state.entries[file.path], { content: raw }); @@ -86,7 +86,7 @@ export default { }); }, [types.UPDATE_FILE_CONTENT](state, { path, content }) { - const stagedFile = state.stagedFiles.find(f => f.path === path); + const stagedFile = state.stagedFiles.find((f) => f.path === path); const rawContent = stagedFile ? stagedFile.content : state.entries[path].raw; const changed = content !== rawContent; @@ -112,7 +112,7 @@ export default { }); }, [types.DISCARD_FILE_CHANGES](state, path) { - const stagedFile = state.stagedFiles.find(f => f.path === path); + const stagedFile = state.stagedFiles.find((f) => f.path === path); const entry = state.entries[path]; const { deleted } = entry; @@ -137,14 +137,14 @@ export default { }, [types.REMOVE_FILE_FROM_CHANGED](state, path) { Object.assign(state, { - changedFiles: state.changedFiles.filter(f => f.path !== path), + changedFiles: state.changedFiles.filter((f) => f.path !== path), }); }, [types.STAGE_CHANGE](state, { path, diffInfo }) { - const stagedFile = state.stagedFiles.find(f => f.path === path); + const stagedFile = state.stagedFiles.find((f) => f.path === path); Object.assign(state, { - changedFiles: state.changedFiles.filter(f => f.path !== path), + changedFiles: state.changedFiles.filter((f) => f.path !== path), entries: Object.assign(state.entries, { [path]: Object.assign(state.entries[path], { staged: diffInfo.exists, @@ -162,12 +162,12 @@ export default { } if (!diffInfo.exists) { - state.stagedFiles = state.stagedFiles.filter(f => f.path !== path); + state.stagedFiles = state.stagedFiles.filter((f) => f.path !== path); } }, [types.UNSTAGE_CHANGE](state, { path, diffInfo }) { - const changedFile = state.changedFiles.find(f => f.path === path); - const stagedFile = state.stagedFiles.find(f => f.path === path); + const changedFile = state.changedFiles.find((f) => f.path === path); + const stagedFile = state.stagedFiles.find((f) => f.path === path); if (!changedFile && stagedFile) { Object.assign(state.entries[path], { @@ -182,11 +182,11 @@ export default { } if (!diffInfo.exists) { - state.changedFiles = state.changedFiles.filter(f => f.path !== path); + state.changedFiles = state.changedFiles.filter((f) => f.path !== path); } Object.assign(state, { - stagedFiles: state.stagedFiles.filter(f => f.path !== path), + stagedFiles: state.stagedFiles.filter((f) => f.path !== path), entries: Object.assign(state.entries, { [path]: Object.assign(state.entries[path], { staged: false, @@ -206,7 +206,7 @@ export default { state.entries[file.path].opened = false; state.entries[file.path].active = false; state.entries[file.path].lastOpenedAt = new Date().getTime(); - state.openFiles.forEach(f => + state.openFiles.forEach((f) => Object.assign(f, { opened: false, active: false, @@ -224,13 +224,13 @@ export default { }, [types.REMOVE_PENDING_TAB](state, file) { Object.assign(state, { - openFiles: state.openFiles.filter(f => f.key !== file.key), + openFiles: state.openFiles.filter((f) => f.key !== file.key), }); }, [types.REMOVE_FILE_FROM_STAGED_AND_CHANGED](state, file) { Object.assign(state, { - changedFiles: state.changedFiles.filter(f => f.key !== file.key), - stagedFiles: state.stagedFiles.filter(f => f.key !== file.key), + changedFiles: state.changedFiles.filter((f) => f.key !== file.key), + stagedFiles: state.stagedFiles.filter((f) => f.key !== file.key), }); Object.assign(state.entries[file.path], { diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js index cce43a99bd9..c38002bd4e0 100644 --- a/app/assets/javascripts/ide/stores/mutations/tree.js +++ b/app/assets/javascripts/ide/stores/mutations/tree.js @@ -45,7 +45,7 @@ export default { ? state.entries[entry.parentPath] : state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; - if (!parent.tree.find(f => f.path === path)) { + if (!parent.tree.find((f) => f.path === path)) { parent.tree = sortTree(parent.tree.concat(entry)); } }, diff --git a/app/assets/javascripts/ide/stores/plugins/terminal.js b/app/assets/javascripts/ide/stores/plugins/terminal.js index 66539c7bd4f..94139d5bdf0 100644 --- a/app/assets/javascripts/ide/stores/plugins/terminal.js +++ b/app/assets/javascripts/ide/stores/plugins/terminal.js @@ -11,7 +11,7 @@ function getPathsFromData(el) { } export default function createTerminalPlugin(el) { - return store => { + return (store) => { store.registerModule('terminal', terminalModule()); store.dispatch('terminal/setPaths', getPathsFromData(el)); diff --git a/app/assets/javascripts/ide/stores/plugins/terminal_sync.js b/app/assets/javascripts/ide/stores/plugins/terminal_sync.js index c60bba4293a..d925a5f7567 100644 --- a/app/assets/javascripts/ide/stores/plugins/terminal_sync.js +++ b/app/assets/javascripts/ide/stores/plugins/terminal_sync.js @@ -1,5 +1,6 @@ import { debounce } from 'lodash'; import eventHub from '~/ide/eventhub'; +import { commitActionTypes } from '~/ide/constants'; import terminalSyncModule from '../modules/terminal_sync'; import { isEndingStatus, isRunningStatus } from '../modules/terminal/utils'; @@ -12,23 +13,32 @@ const UPLOAD_DEBOUNCE = 200; * - Listens for file change event to control upload. */ export default function createMirrorPlugin() { - return store => { + return (store) => { store.registerModule('terminalSync', terminalSyncModule()); const upload = debounce(() => { store.dispatch(`terminalSync/upload`); }, UPLOAD_DEBOUNCE); + const onFilesChange = (payload) => { + // Do nothing on a file update since we only want to trigger manually on "save". + if (payload?.type === commitActionTypes.update) { + return; + } + + upload(); + }; + const stop = () => { store.dispatch(`terminalSync/stop`); - eventHub.$off('ide.files.change', upload); + eventHub.$off('ide.files.change', onFilesChange); }; const start = () => { store .dispatch(`terminalSync/start`) .then(() => { - eventHub.$on('ide.files.change', upload); + eventHub.$on('ide.files.change', onFilesChange); }) .catch(() => { // error is handled in store @@ -36,8 +46,8 @@ export default function createMirrorPlugin() { }; store.watch( - x => x.terminal && x.terminal.session && x.terminal.session.status, - val => { + (x) => x.terminal && x.terminal.session && x.terminal.session.status, + (val) => { if (isRunningStatus(val)) { start(); } else if (isEndingStatus(val)) { diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index 96f3caf1e98..04eacf271b8 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -31,9 +31,10 @@ export const dataStructure = () => ({ mrChange: null, deleted: false, prevPath: undefined, + mimeType: '', }); -export const decorateData = entity => { +export const decorateData = (entity) => { const { id, type, @@ -47,6 +48,7 @@ export const decorateData = entity => { rawPath = '', file_lock, parentPath = '', + mimeType = '', } = entity; return Object.assign(dataStructure(), { @@ -63,10 +65,11 @@ export const decorateData = entity => { rawPath, file_lock, parentPath, + mimeType, }); }; -export const setPageTitle = title => { +export const setPageTitle = (title) => { document.title = title; }; @@ -75,7 +78,7 @@ export const setPageTitleForFile = (state, file) => { setPageTitle(title); }; -export const commitActionForFile = file => { +export const commitActionForFile = (file) => { if (file.prevPath) { return commitActionTypes.move; } else if (file.deleted) { @@ -87,7 +90,7 @@ export const commitActionForFile = file => { return commitActionTypes.update; }; -export const getCommitFiles = stagedFiles => +export const getCommitFiles = (stagedFiles) => stagedFiles.reduce((acc, file) => { if (file.type === 'tree') return acc; @@ -106,7 +109,7 @@ export const createCommitPayload = ({ }) => ({ branch, commit_message: state.commitMessage || getters.preBuiltCommitMessage, - actions: getCommitFiles(rootState.stagedFiles).map(f => { + actions: getCommitFiles(rootState.stagedFiles).map((f) => { const isBlob = isBlobUrl(f.rawPath); const content = isBlob ? btoa(f.content) : f.content; @@ -136,9 +139,9 @@ const sortTreesByTypeAndName = (a, b) => { return 0; }; -export const sortTree = sortedTree => +export const sortTree = (sortedTree) => sortedTree - .map(entity => + .map((entity) => Object.assign(entity, { tree: entity.tree.length ? sortTree(entity.tree) : [], }), @@ -148,7 +151,7 @@ export const sortTree = sortedTree => export const filePathMatches = (filePath, path) => filePath.indexOf(`${path}/`) === 0; export const getChangesCountForFiles = (files, path) => - files.filter(f => filePathMatches(f.path, path)).length; + files.filter((f) => filePathMatches(f.path, path)).length; export const mergeTrees = (fromTree, toTree) => { if (!fromTree || !fromTree.length) { @@ -159,7 +162,7 @@ export const mergeTrees = (fromTree, toTree) => { if (!n) { return t; } - const existingTreeNode = t.find(el => el.path === n.path); + const existingTreeNode = t.find((el) => el.path === n.path); if (existingTreeNode && n.tree.length > 0) { existingTreeNode.opened = true; @@ -180,7 +183,7 @@ export const mergeTrees = (fromTree, toTree) => { export const swapInStateArray = (state, arr, key, entryPath) => Object.assign(state, { - [arr]: state[arr].map(f => (f.key === key ? state.entries[entryPath] : f)), + [arr]: state[arr].map((f) => (f.key === key ? state.entries[entryPath] : f)), }); export const getEntryOrRoot = (state, path) => @@ -213,12 +216,12 @@ export const removeFromParentTree = (state, oldKey, parentPath) => { }; export const updateFileCollections = (state, key, entryPath) => { - ['openFiles', 'changedFiles', 'stagedFiles'].forEach(fileCollection => { + ['openFiles', 'changedFiles', 'stagedFiles'].forEach((fileCollection) => { swapInStateArray(state, fileCollection, key, entryPath); }); }; -export const cleanTrailingSlash = path => path.replace(/\/$/, ''); +export const cleanTrailingSlash = (path) => path.replace(/\/$/, ''); export const pathsAreEqual = (a, b) => { const cleanA = a ? cleanTrailingSlash(a) : ''; @@ -251,12 +254,7 @@ export function extractMarkdownImagesFromEntries(mdFile, entries) { const imageContent = entries[imagePath]?.content || entries[imagePath]?.raw; if (!isAbsolute(path) && imageContent) { - const ext = path.includes('.') - ? path - .split('.') - .pop() - .trim() - : 'jpeg'; + const ext = path.includes('.') ? path.split('.').pop().trim() : 'jpeg'; const src = `data:image/${ext};base64,${imageContent}`; i += 1; const key = `{{${prefix}${i}}}`; diff --git a/app/assets/javascripts/ide/sync_router_and_store.js b/app/assets/javascripts/ide/sync_router_and_store.js index b33bcbb94ea..d73ac93dc1d 100644 --- a/app/assets/javascripts/ide/sync_router_and_store.js +++ b/app/assets/javascripts/ide/sync_router_and_store.js @@ -21,8 +21,8 @@ export const syncRouterAndStore = (router, store) => { // sync store to router disposables.push( store.watch( - state => state.router.fullPath, - fullPath => { + (state) => state.router.fullPath, + (fullPath) => { if (currentPath === fullPath) { return; } @@ -36,7 +36,7 @@ export const syncRouterAndStore = (router, store) => { // sync router to store disposables.push( - router.afterEach(to => { + router.afterEach((to) => { if (currentPath === to.fullPath) { return; } @@ -47,7 +47,7 @@ export const syncRouterAndStore = (router, store) => { ); const unsync = () => { - disposables.forEach(fn => fn()); + disposables.forEach((fn) => fn()); }; return unsync; diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js index 43276f32322..8eb2d17b876 100644 --- a/app/assets/javascripts/ide/utils.js +++ b/app/assets/javascripts/ide/utils.js @@ -3,17 +3,17 @@ import { flatten, isString } from 'lodash'; import { SIDE_LEFT, SIDE_RIGHT } from './constants'; import { performanceMarkAndMeasure } from '~/performance/utils'; -const toLowerCase = x => x.toLowerCase(); +const toLowerCase = (x) => x.toLowerCase(); const monacoLanguages = languages.getLanguages(); const monacoExtensions = new Set( - flatten(monacoLanguages.map(lang => lang.extensions?.map(toLowerCase) || [])), + flatten(monacoLanguages.map((lang) => lang.extensions?.map(toLowerCase) || [])), ); const monacoMimetypes = new Set( - flatten(monacoLanguages.map(lang => lang.mimetypes?.map(toLowerCase) || [])), + flatten(monacoLanguages.map((lang) => lang.mimetypes?.map(toLowerCase) || [])), ); const monacoFilenames = new Set( - flatten(monacoLanguages.map(lang => lang.filenames?.map(toLowerCase) || [])), + flatten(monacoLanguages.map((lang) => lang.filenames?.map(toLowerCase) || [])), ); const KNOWN_TYPES = [ @@ -44,7 +44,7 @@ const KNOWN_TYPES = [ ]; export function isTextFile({ name, raw, content, mimeType = '' }) { - const knownType = KNOWN_TYPES.find(type => type.isMatch(mimeType, name)); + const knownType = KNOWN_TYPES.find((type) => type.isMatch(mimeType, name)); if (knownType) return knownType.isText; // does the string contain ascii characters only (ranges from space to tilde, tabs and new lines) @@ -56,20 +56,20 @@ export function isTextFile({ name, raw, content, mimeType = '' }) { return isString(fileContents) && (fileContents === '' || asciiRegex.test(fileContents)); } -export const createPathWithExt = p => { +export const createPathWithExt = (p) => { const ext = p.lastIndexOf('.') >= 0 ? p.substring(p.lastIndexOf('.') + 1) : ''; return `${p.substring(1, p.lastIndexOf('.') + 1 || p.length)}${ext || '.js'}`; }; -export const trimPathComponents = path => +export const trimPathComponents = (path) => path .split('/') - .map(s => s.trim()) + .map((s) => s.trim()) .join('/'); export function registerLanguages(def, ...defs) { - defs.forEach(lang => registerLanguages(lang)); + defs.forEach((lang) => registerLanguages(lang)); const languageId = def.id; @@ -80,7 +80,7 @@ export function registerLanguages(def, ...defs) { export function registerSchema(schema) { const defaults = [languages.json.jsonDefaults, languages.yaml.yamlDefaults]; - defaults.forEach(d => + defaults.forEach((d) => d.setDiagnosticsOptions({ validate: true, enableSchemaRequest: true, @@ -91,7 +91,7 @@ export function registerSchema(schema) { ); } -export const otherSide = side => (side === SIDE_RIGHT ? SIDE_LEFT : SIDE_RIGHT); +export const otherSide = (side) => (side === SIDE_RIGHT ? SIDE_LEFT : SIDE_RIGHT); export function trimTrailingWhitespace(content) { return content.replace(/[^\S\r\n]+$/gm, ''); @@ -125,9 +125,9 @@ export function getPathParent(path) { * @param {File} file */ export function readFileAsDataURL(file) { - return new Promise(resolve => { + return new Promise((resolve) => { const reader = new FileReader(); - reader.addEventListener('load', e => resolve(e.target.result), { once: true }); + reader.addEventListener('load', (e) => resolve(e.target.result), { once: true }); reader.readAsDataURL(file); }); } @@ -154,12 +154,7 @@ export function getFileEOL(content = '') { */ export function addNumericSuffix(filename, randomize = false) { return filename.replace(/([ _-]?)(\d*)(\..+?$|$)/, (_, before, number, after) => { - const n = randomize - ? Math.random() - .toString() - .substring(2, 7) - .slice(-5) - : Number(number) + 1; + const n = randomize ? Math.random().toString().substring(2, 7).slice(-5) : Number(number) + 1; return `${before || '-'}${n}${after}`; }); } diff --git a/app/assets/javascripts/image_diff/helpers/badge_helper.js b/app/assets/javascripts/image_diff/helpers/badge_helper.js index 229e0a62c51..8ee72235a23 100644 --- a/app/assets/javascripts/image_diff/helpers/badge_helper.js +++ b/app/assets/javascripts/image_diff/helpers/badge_helper.js @@ -3,7 +3,7 @@ import { spriteIcon } from '~/lib/utils/common_utils'; export function createImageBadge(noteId, { x, y }, classNames = []) { const buttonEl = document.createElement('button'); const classList = classNames.concat(['js-image-badge']); - classList.forEach(className => buttonEl.classList.add(className)); + classList.forEach((className) => buttonEl.classList.add(className)); buttonEl.setAttribute('type', 'button'); buttonEl.setAttribute('disabled', true); buttonEl.dataset.noteId = noteId; diff --git a/app/assets/javascripts/image_diff/init_discussion_tab.js b/app/assets/javascripts/image_diff/init_discussion_tab.js index 54ff2858206..4e0c5a6d916 100644 --- a/app/assets/javascripts/image_diff/init_discussion_tab.js +++ b/app/assets/javascripts/image_diff/init_discussion_tab.js @@ -7,7 +7,7 @@ export default () => { const renderCommentBadge = true; const diffFileEls = document.querySelectorAll('.timeline-content .diff-file.js-image-file'); - [...diffFileEls].forEach(diffFileEl => + [...diffFileEls].forEach((diffFileEl) => initImageDiffHelper.initImageDiff(diffFileEl, canCreateNote, renderCommentBadge), ); }; diff --git a/app/assets/javascripts/image_diff/replaced_image_diff.js b/app/assets/javascripts/image_diff/replaced_image_diff.js index 8d9e65155d8..2df15e5e1a5 100644 --- a/app/assets/javascripts/image_diff/replaced_image_diff.js +++ b/app/assets/javascripts/image_diff/replaced_image_diff.js @@ -26,7 +26,7 @@ export default class ReplacedImageDiff extends ImageDiff { this.imageEls = {}; const viewTypeNames = Object.getOwnPropertyNames(viewTypes); - viewTypeNames.forEach(viewType => { + viewTypeNames.forEach((viewType) => { this.imageEls[viewType] = this.imageFrameEls[viewType].querySelector('img'); }); } @@ -62,7 +62,7 @@ export default class ReplacedImageDiff extends ImageDiff { // Clear existing badges on new view const existingBadges = this.imageFrameEl.querySelectorAll('.badge'); - [...existingBadges].map(badge => badge.remove()); + [...existingBadges].map((badge) => badge.remove()); // Remove existing references to old view image badges this.imageBadges = []; diff --git a/app/assets/javascripts/image_diff/view_types.js b/app/assets/javascripts/image_diff/view_types.js index 1a5123de220..f08b29e468c 100644 --- a/app/assets/javascripts/image_diff/view_types.js +++ b/app/assets/javascripts/image_diff/view_types.js @@ -5,5 +5,5 @@ export const viewTypes = { }; export function isValidViewType(validate) { - return Boolean(Object.getOwnPropertyNames(viewTypes).find(viewType => viewType === validate)); + return Boolean(Object.getOwnPropertyNames(viewTypes).find((viewType) => viewType === validate)); } diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue index 153c58b556e..80e2e73f420 100644 --- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue +++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue @@ -7,7 +7,7 @@ import setNewNameMutation from '../graphql/mutations/set_new_name.mutation.graph import importGroupMutation from '../graphql/mutations/import_group.mutation.graphql'; import ImportTableRow from './import_table_row.vue'; -const mapApolloMutations = mutations => +const mapApolloMutations = (mutations) => Object.fromEntries( Object.entries(mutations).map(([key, mutation]) => [ key, diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue index 07603d89f0f..1707ab10c89 100644 --- a/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue +++ b/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue @@ -35,7 +35,7 @@ export default { select2Options() { return { - data: this.availableNamespaces.map(namespace => ({ + data: this.availableNamespaces.map((namespace) => ({ id: namespace.full_path, text: namespace.full_path, })), diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js index 4fcaa1b55fc..8f2d488d661 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js +++ b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js @@ -23,7 +23,7 @@ export function createResolvers({ endpoints }) { } = await client.query({ query: availableNamespacesQuery }); return axios.get(endpoints.status).then(({ data }) => { - return data.importable_data.map(group => ({ + return data.importable_data.map((group) => ({ __typename: clientTypenames.BulkImportSourceGroup, ...group, status: STATUSES.NONE, @@ -37,7 +37,7 @@ export function createResolvers({ endpoints }) { availableNamespaces: () => axios.get(endpoints.availableNamespaces).then(({ data }) => - data.map(namespace => ({ + data.map((namespace) => ({ __typename: clientTypenames.AvailableNamespace, ...namespace, })), @@ -45,14 +45,14 @@ export function createResolvers({ endpoints }) { }, Mutation: { setTargetNamespace(_, { targetNamespace, sourceGroupId }, { client }) { - new SourceGroupsManager({ client }).updateById(sourceGroupId, sourceGroup => { + new SourceGroupsManager({ client }).updateById(sourceGroupId, (sourceGroup) => { // eslint-disable-next-line no-param-reassign sourceGroup.import_target.target_namespace = targetNamespace; }); }, setNewName(_, { newName, sourceGroupId }, { client }) { - new SourceGroupsManager({ client }).updateById(sourceGroupId, sourceGroup => { + new SourceGroupsManager({ client }).updateById(sourceGroupId, (sourceGroup) => { // eslint-disable-next-line no-param-reassign sourceGroup.import_target.new_name = newName; }); diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js index f752ecc8cd6..047b04fe7d6 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js +++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js @@ -37,7 +37,7 @@ export class SourceGroupsManager { } setImportStatus(group, status) { - this.update(group, sourceGroup => { + this.update(group, (sourceGroup) => { // eslint-disable-next-line no-param-reassign sourceGroup.status = status; }); diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js index 5d2922b0ba8..41dd25b9150 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js +++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js @@ -5,7 +5,7 @@ import bulkImportSourceGroupsQuery from '../queries/bulk_import_source_groups.qu import { STATUSES } from '../../../constants'; import { SourceGroupsManager } from './source_groups_manager'; -const groupId = i => `group${i}`; +const groupId = (i) => `group${i}`; function generateGroupsQuery(groups) { return gql`{ @@ -46,14 +46,14 @@ export class StatusPoller { const { bulkImportSourceGroups } = this.client.readQuery({ query: bulkImportSourceGroupsQuery, }); - const groupsInProgress = bulkImportSourceGroups.filter(g => g.status === STATUSES.STARTED); + const groupsInProgress = bulkImportSourceGroups.filter((g) => g.status === STATUSES.STARTED); if (groupsInProgress.length) { const { data: results } = await this.client.query({ query: generateGroupsQuery(groupsInProgress), fetchPolicy: 'no-cache', }); const completedGroups = groupsInProgress.filter((_, idx) => Boolean(results[groupId(idx)])); - completedGroups.forEach(group => { + completedGroups.forEach((group) => { this.groupManager.setImportStatus(group, STATUSES.FINISHED); }); } diff --git a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue index 2b6b8b765a2..192d6e056cd 100644 --- a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue +++ b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue @@ -35,6 +35,7 @@ export default { ...mapGetters([ 'isLoading', 'isImportingAnyRepo', + 'importingRepoCount', 'hasImportableRepos', 'hasIncompatibleRepos', 'importAllCount', @@ -60,13 +61,17 @@ export default { }, importAllButtonText() { - return this.hasIncompatibleRepos - ? n__( - 'Import %d compatible repository', - 'Import %d compatible repositories', - this.importAllCount, - ) - : n__('Import %d repository', 'Import %d repositories', this.importAllCount); + if (this.isImportingAnyRepo) { + return n__('Importing %d repository', 'Importing %d repositories', this.importingRepoCount); + } + + if (this.hasIncompatibleRepos) + return n__( + 'Import %d compatible repository', + 'Import %d compatible repositories', + this.importAllCount, + ); + return n__('Import %d repository', 'Import %d repositories', this.importAllCount); }, emptyStateText() { diff --git a/app/assets/javascripts/import_entities/import_projects/store/actions.js b/app/assets/javascripts/import_entities/import_projects/store/actions.js index 7b7afd13c55..a8217ff1033 100644 --- a/app/assets/javascripts/import_entities/import_projects/store/actions.js +++ b/app/assets/javascripts/import_entities/import_projects/store/actions.js @@ -12,9 +12,9 @@ import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; let eTagPoll; -const hasRedirectInError = e => e?.response?.data?.error?.redirect; -const redirectToUrlInError = e => visitUrl(e.response.data.error.redirect); -const tooManyRequests = e => e.response.status === httpStatusCodes.TOO_MANY_REQUESTS; +const hasRedirectInError = (e) => e?.response?.data?.error?.redirect; +const redirectToUrlInError = (e) => visitUrl(e.response.data.error.redirect); +const tooManyRequests = (e) => e.response.status === httpStatusCodes.TOO_MANY_REQUESTS; const pathWithParams = ({ path, ...params }) => { const filteredParams = Object.fromEntries( Object.entries(params).filter(([, value]) => value !== ''), @@ -47,7 +47,7 @@ const importAll = ({ state, dispatch }) => { return Promise.all( state.repositories .filter(isProjectImportable) - .map(r => dispatch('fetchImport', r.importSource.id)), + .map((r) => dispatch('fetchImport', r.importSource.id)), ); }; @@ -69,7 +69,7 @@ const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit }) .then(({ data }) => { commit(types.RECEIVE_REPOS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })); }) - .catch(e => { + .catch((e) => { commit(types.SET_PAGE, nextPage - 1); if (hasRedirectInError(e)) { @@ -114,7 +114,7 @@ const fetchImportFactory = (importPath = isRequired()) => ({ state, commit, gett repoId, }); }) - .catch(e => { + .catch((e) => { const serverErrorMessage = e?.response?.data?.errors; const flashMessage = serverErrorMessage ? sprintf( @@ -145,7 +145,7 @@ export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, d method: 'fetchJobs', successCallback: ({ data }) => commit(types.RECEIVE_JOBS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })), - errorCallback: e => { + errorCallback: (e) => { if (hasRedirectInError(e)) { redirectToUrlInError(e); } else { diff --git a/app/assets/javascripts/import_entities/import_projects/store/getters.js b/app/assets/javascripts/import_entities/import_projects/store/getters.js index 31e22b50554..ef01a67ec94 100644 --- a/app/assets/javascripts/import_entities/import_projects/store/getters.js +++ b/app/assets/javascripts/import_entities/import_projects/store/getters.js @@ -1,27 +1,23 @@ -import { STATUSES } from '../../constants'; -import { isProjectImportable, isIncompatible } from '../utils'; +import { isProjectImportable, isIncompatible, isImporting } from '../utils'; -export const isLoading = state => state.isLoadingRepos || state.isLoadingNamespaces; +export const isLoading = (state) => state.isLoadingRepos || state.isLoadingNamespaces; -export const isImportingAnyRepo = state => - state.repositories.some(repo => - [STATUSES.SCHEDULING, STATUSES.SCHEDULED, STATUSES.STARTED].includes( - repo.importedProject?.importStatus, - ), - ); +export const importingRepoCount = (state) => state.repositories.filter(isImporting).length; -export const hasIncompatibleRepos = state => state.repositories.some(isIncompatible); +export const isImportingAnyRepo = (state) => state.repositories.some(isImporting); -export const hasImportableRepos = state => state.repositories.some(isProjectImportable); +export const hasIncompatibleRepos = (state) => state.repositories.some(isIncompatible); -export const importAllCount = state => state.repositories.filter(isProjectImportable).length; +export const hasImportableRepos = (state) => state.repositories.some(isProjectImportable); -export const getImportTarget = state => repoId => { +export const importAllCount = (state) => state.repositories.filter(isProjectImportable).length; + +export const getImportTarget = (state) => (repoId) => { if (state.customImportTargets[repoId]) { return state.customImportTargets[repoId]; } - const repo = state.repositories.find(r => r.importSource.id === repoId); + const repo = state.repositories.find((r) => r.importSource.id === repoId); return { newName: repo.importSource.sanitizedName, diff --git a/app/assets/javascripts/import_entities/import_projects/store/mutations.js b/app/assets/javascripts/import_entities/import_projects/store/mutations.js index 3d718a6a386..1a96508bd48 100644 --- a/app/assets/javascripts/import_entities/import_projects/store/mutations.js +++ b/app/assets/javascripts/import_entities/import_projects/store/mutations.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import * as types from './mutation_types'; import { STATUSES } from '../../constants'; -const makeNewImportedProject = importedProject => ({ +const makeNewImportedProject = (importedProject) => ({ importSource: { id: importedProject.id, fullName: importedProject.importSource, @@ -12,15 +12,15 @@ const makeNewImportedProject = importedProject => ({ importedProject, }); -const makeNewIncompatibleProject = project => ({ +const makeNewIncompatibleProject = (project) => ({ importSource: { ...project, incompatible: true }, importedProject: null, }); const processLegacyEntries = ({ newRepositories, existingRepositories, factory }) => { const newEntries = []; - newRepositories.forEach(project => { - const existingProject = existingRepositories.find(p => p.importSource.id === project.id); + newRepositories.forEach((project) => { + const existingProject = existingRepositories.find((p) => p.importSource.id === project.id); const importedProjectShape = factory(project); if (existingProject) { @@ -66,7 +66,7 @@ export default { state.repositories = [ ...newImportedProjects, ...state.repositories, - ...repositories.providerRepos.map(project => ({ + ...repositories.providerRepos.map((project) => ({ importSource: project, importedProject: null, })), @@ -91,7 +91,7 @@ export default { }, [types.REQUEST_IMPORT](state, { repoId, importTarget }) { - const existingRepo = state.repositories.find(r => r.importSource.id === repoId); + const existingRepo = state.repositories.find((r) => r.importSource.id === repoId); existingRepo.importedProject = { importStatus: STATUSES.SCHEDULING, fullPath: `/${importTarget.targetNamespace}/${importTarget.newName}`, @@ -99,18 +99,18 @@ export default { }, [types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId }) { - const existingRepo = state.repositories.find(r => r.importSource.id === repoId); + const existingRepo = state.repositories.find((r) => r.importSource.id === repoId); existingRepo.importedProject = importedProject; }, [types.RECEIVE_IMPORT_ERROR](state, repoId) { - const existingRepo = state.repositories.find(r => r.importSource.id === repoId); + const existingRepo = state.repositories.find((r) => r.importSource.id === repoId); existingRepo.importedProject = null; }, [types.RECEIVE_JOBS_SUCCESS](state, updatedProjects) { - updatedProjects.forEach(updatedProject => { - const repo = state.repositories.find(p => p.importedProject?.id === updatedProject.id); + updatedProjects.forEach((updatedProject) => { + const repo = state.repositories.find((p) => p.importedProject?.id === updatedProject.id); if (repo?.importedProject) { repo.importedProject.importStatus = updatedProject.importStatus; } @@ -131,7 +131,7 @@ export default { }, [types.SET_IMPORT_TARGET](state, { repoId, importTarget }) { - const existingRepo = state.repositories.find(r => r.importSource.id === repoId); + const existingRepo = state.repositories.find((r) => r.importSource.id === repoId); if ( importTarget.targetNamespace === state.defaultTargetNamespace && diff --git a/app/assets/javascripts/import_entities/import_projects/utils.js b/app/assets/javascripts/import_entities/import_projects/utils.js index 0610117e09b..38bd529321a 100644 --- a/app/assets/javascripts/import_entities/import_projects/utils.js +++ b/app/assets/javascripts/import_entities/import_projects/utils.js @@ -11,3 +11,9 @@ export function getImportStatus(project) { export function isProjectImportable(project) { return !isIncompatible(project) && getImportStatus(project) === STATUSES.NONE; } + +export function isImporting(repo) { + return [STATUSES.SCHEDULING, STATUSES.SCHEDULED, STATUSES.STARTED].includes( + repo.importedProject?.importStatus, + ); +} diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue index 0e3839deaf5..7d44a28b4bb 100644 --- a/app/assets/javascripts/incidents/components/incidents_list.vue +++ b/app/assets/javascripts/incidents/components/incidents_list.vue @@ -265,9 +265,7 @@ export default { if (field?.sortKey) { sortKey = field.sortKey; } else { - sortKey = convertToSnakeCase(sortBy) - .replace(/_.*/, '') - .toUpperCase(); + sortKey = convertToSnakeCase(sortBy).replace(/_.*/, '').toUpperCase(); } this.pagination = initialPaginationState; diff --git a/app/assets/javascripts/incidents/list.js b/app/assets/javascripts/incidents/list.js index 6f87fbbe775..8644ff3a249 100644 --- a/app/assets/javascripts/incidents/list.js +++ b/app/assets/javascripts/incidents/list.js @@ -29,6 +29,9 @@ export default () => { return new Vue({ el: selector, + components: { + IncidentsList, + }, provide: { projectPath, incidentTemplateName, @@ -43,9 +46,6 @@ export default () => { slaFeatureAvailable: parseBoolean(slaFeatureAvailable), }, apolloProvider, - components: { - IncidentsList, - }, render(createElement) { return createElement('incidents-list'); }, diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js index c0dc6ce07b1..22667d8ae88 100644 --- a/app/assets/javascripts/init_changes_dropdown.js +++ b/app/assets/javascripts/init_changes_dropdown.js @@ -2,7 +2,7 @@ import $ from 'jquery'; import { stickyMonitor } from './lib/utils/sticky'; import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; -export default stickyTop => { +export default (stickyTop) => { stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop); initDeprecatedJQueryDropdown($('.js-diff-stats-dropdown'), { diff --git a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue index 9dde1ed1055..f568f7e6d3d 100644 --- a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue +++ b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue @@ -92,7 +92,7 @@ export default { return isEmpty(this.value) && this.required; }, options() { - return this.choices.map(choice => { + return this.choices.map((choice) => { return { value: choice[1], text: choice[0], diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue index c6f8ba8dcb2..ac8a64d5f3b 100644 --- a/app/assets/javascripts/integrations/edit/components/integration_form.vue +++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue @@ -59,9 +59,6 @@ export default { showReset() { return this.isInstanceOrGroupLevel && this.propsSource.resetPath; }, - saveButtonKey() { - return `save-button-${this.isDisabled}`; - }, }, methods: { ...mapActions([ @@ -120,7 +117,6 @@ export default { <div v-if="isEditable" class="footer-block row-content-block"> <template v-if="isInstanceOrGroupLevel"> <gl-button - :key="saveButtonKey" v-gl-modal.confirmSaveIntegration category="primary" variant="success" @@ -134,7 +130,6 @@ export default { </template> <gl-button v-else - :key="saveButtonKey" category="primary" variant="success" type="submit" diff --git a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue index 123d794912a..bc005aa16e9 100644 --- a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue +++ b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue @@ -91,6 +91,8 @@ export default { <gl-form-group v-show="showEnableComments" :label="s__('Integrations|Comment settings:')" + label-for="service[comment_on_event_enabled]" + class="gl-pl-6" data-testid="comment-settings" > <input @@ -106,6 +108,8 @@ export default { <gl-form-group v-show="showEnableComments && enableComments" :label="s__('Integrations|Comment detail:')" + label-for="service[comment_detail]" + class="gl-pl-9" data-testid="comment-detail" > <input name="service[comment_detail]" type="hidden" :value="commentDetail" /> diff --git a/app/assets/javascripts/integrations/edit/components/override_dropdown.vue b/app/assets/javascripts/integrations/edit/components/override_dropdown.vue index c31dada8d2f..4e2c37ac7f3 100644 --- a/app/assets/javascripts/integrations/edit/components/override_dropdown.vue +++ b/app/assets/javascripts/integrations/edit/components/override_dropdown.vue @@ -40,7 +40,7 @@ export default { }, data() { return { - selected: dropdownOptions.find(x => x.value === this.override), + selected: dropdownOptions.find((x) => x.value === this.override), }; }, computed: { diff --git a/app/assets/javascripts/integrations/edit/store/getters.js b/app/assets/javascripts/integrations/edit/store/getters.js index 310d970c73e..39e14de2d0d 100644 --- a/app/assets/javascripts/integrations/edit/store/getters.js +++ b/app/assets/javascripts/integrations/edit/store/getters.js @@ -1,6 +1,6 @@ -export const isInheriting = state => (state.defaultState === null ? false : !state.override); +export const isInheriting = (state) => (state.defaultState === null ? false : !state.override); -export const isDisabled = state => state.isSaving || state.isTesting || state.isResetting; +export const isDisabled = (state) => state.isSaving || state.isTesting || state.isResetting; export const propsSource = (state, getters) => getters.isInheriting ? state.defaultState : state.customState; diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js index 14d6f133d27..861655a6a64 100644 --- a/app/assets/javascripts/integrations/integration_settings_form.js +++ b/app/assets/javascripts/integrations/integration_settings_form.js @@ -1,4 +1,5 @@ import $ from 'jquery'; +import { delay } from 'lodash'; import axios from '../lib/utils/axios_utils'; import { __, s__ } from '~/locale'; import toast from '~/vue_shared/plugins/global_toast'; @@ -22,7 +23,7 @@ export default class IntegrationSettingsForm { document.querySelector('.js-vue-integration-settings'), document.querySelector('.js-vue-default-integration-settings'), ); - eventHub.$on('toggle', active => { + eventHub.$on('toggle', (active) => { this.formActive = active; this.toggleServiceState(); }); @@ -43,7 +44,9 @@ export default class IntegrationSettingsForm { const formValid = this.$form.get(0).checkValidity() || this.formActive === false; if (formValid) { - this.$form.submit(); + delay(() => { + this.$form.trigger('submit'); + }, 100); } else { eventHub.$emit('validateForm'); this.vue.$store.dispatch('setIsSaving', false); diff --git a/app/assets/javascripts/invite_member/init_invite_member_modal.js b/app/assets/javascripts/invite_member/init_invite_member_modal.js index 7d60d78d3d9..901e3e315ee 100644 --- a/app/assets/javascripts/invite_member/init_invite_member_modal.js +++ b/app/assets/javascripts/invite_member/init_invite_member_modal.js @@ -16,6 +16,6 @@ export default function initInviteMembersModal() { return new Vue({ el, provide: { membersPath }, - render: createElement => createElement(InviteMemberModal), + render: (createElement) => createElement(InviteMemberModal), }); } diff --git a/app/assets/javascripts/invite_member/init_invite_member_trigger.js b/app/assets/javascripts/invite_member/init_invite_member_trigger.js index a5f904b87a6..5e763e4f47d 100644 --- a/app/assets/javascripts/invite_member/init_invite_member_trigger.js +++ b/app/assets/javascripts/invite_member/init_invite_member_trigger.js @@ -11,6 +11,6 @@ export default function initInviteMembersTrigger() { return new Vue({ el, provide: { ...el.dataset }, - render: createElement => createElement(InviteMemberTrigger), + render: (createElement) => createElement(InviteMemberTrigger), }); } diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue index b55ef77ae5d..a92289ca8c1 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue @@ -9,6 +9,7 @@ import { GlButton, GlFormInput, } from '@gitlab/ui'; +import { partition, isString } from 'lodash'; import eventHub from '../event_hub'; import { s__, __, sprintf } from '~/locale'; import Api from '~/api'; @@ -58,7 +59,7 @@ export default { visible: true, modalId: 'invite-members-modal', selectedAccessLevel: this.defaultAccessLevel, - newUsersToInvite: '', + newUsersToInvite: [], selectedDate: undefined, }; }, @@ -79,13 +80,12 @@ export default { return { onComplete: () => { this.selectedAccessLevel = this.defaultAccessLevel; - this.newUsersToInvite = ''; + this.newUsersToInvite = []; }, }; }, - postData() { + basePostData() { return { - user_id: this.newUsersToInvite, access_level: this.selectedAccessLevel, expires_at: this.selectedDate, format: 'json', @@ -93,7 +93,7 @@ export default { }, selectedRoleName() { return Object.keys(this.accessLevels).find( - key => this.accessLevels[key] === Number(this.selectedAccessLevel), + (key) => this.accessLevels[key] === Number(this.selectedAccessLevel), ); }, }, @@ -101,6 +101,17 @@ export default { eventHub.$on('openModal', this.openModal); }, methods: { + partitionNewUsersToInvite() { + const [usersToInviteByEmail, usersToAddById] = partition( + this.newUsersToInvite, + (user) => isString(user.id) && user.id.includes('user-defined-token'), + ); + + return [ + usersToInviteByEmail.map((user) => user.name).join(','), + usersToAddById.map((user) => user.id).join(','), + ]; + }, openModal() { this.$root.$emit('bv::show::modal', this.modalId); }, @@ -108,7 +119,7 @@ export default { this.$root.$emit('bv::hide::modal', this.modalId); }, sendInvite() { - this.submitForm(this.postData); + this.submitForm(); this.closeModal(); }, cancelInvite() { @@ -120,15 +131,33 @@ export default { changeSelectedItem(item) { this.selectedAccessLevel = item; }, - submitForm(formData) { - if (this.isProject) { - return Api.inviteProjectMembers(this.id, formData) - .then(this.showToastMessageSuccess) - .catch(this.showToastMessageError); + submitForm() { + const [usersToInviteByEmail, usersToAddById] = this.partitionNewUsersToInvite(); + const promises = []; + + if (usersToInviteByEmail !== '') { + const apiInviteByEmail = this.isProject + ? Api.inviteProjectMembersByEmail.bind(Api) + : Api.inviteGroupMembersByEmail.bind(Api); + + promises.push(apiInviteByEmail(this.id, this.inviteByEmailPostData(usersToInviteByEmail))); } - return Api.inviteGroupMember(this.id, formData) - .then(this.showToastMessageSuccess) - .catch(this.showToastMessageError); + + if (usersToAddById !== '') { + const apiAddByUserId = this.isProject + ? Api.addProjectMembersByUserId.bind(Api) + : Api.addGroupMembersByUserId.bind(Api); + + promises.push(apiAddByUserId(this.id, this.addByUserIdPostData(usersToAddById))); + } + + Promise.all(promises).then(this.showToastMessageSuccess).catch(this.showToastMessageError); + }, + inviteByEmailPostData(usersToInviteByEmail) { + return { ...this.basePostData, email: usersToInviteByEmail }; + }, + addByUserIdPostData(usersToAddById) { + return { ...this.basePostData, user_id: usersToAddById }; }, showToastMessageSuccess() { this.$toast.show(this.$options.labels.toastMessageSuccessful, this.toastOptions); @@ -195,7 +224,7 @@ export default { <div class="gl-mt-2"> <gl-sprintf :message="$options.labels.readMoreText"> - <template #link="{content}"> + <template #link="{ content }"> <gl-link :href="helpLink" target="_blank">{{ content }}</gl-link> </template> </gl-sprintf> diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue index d133e3655e3..eb97c458f88 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue @@ -19,6 +19,11 @@ export default { required: false, default: '', }, + classes: { + type: String, + required: false, + default: '', + }, }, methods: { openModal() { @@ -29,7 +34,7 @@ export default { </script> <template> - <gl-link @click="openModal"> + <gl-link :class="classes" @click="openModal"> <div v-if="icon" class="nav-icon-container"> <gl-icon :size="16" :name="icon" /> </div> diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue index aed2e5e3236..627d4ab2771 100644 --- a/app/assets/javascripts/invite_members/components/members_token_select.vue +++ b/app/assets/javascripts/invite_members/components/members_token_select.vue @@ -1,14 +1,16 @@ <script> import { debounce } from 'lodash'; -import { GlTokenSelector, GlAvatar, GlAvatarLabeled } from '@gitlab/ui'; +import { GlTokenSelector, GlAvatar, GlAvatarLabeled, GlSprintf } from '@gitlab/ui'; +import { __ } from '~/locale'; import { USER_SEARCH_DELAY } from '../constants'; -import Api from '~/api'; +import { getUsers } from '~/rest_api'; export default { components: { GlTokenSelector, GlAvatar, GlAvatarLabeled, + GlSprintf, }, props: { placeholder: { @@ -32,12 +34,10 @@ export default { }; }, computed: { - newUsersToInvite() { - return this.selectedTokens - .map(obj => { - return obj.id; - }) - .join(','); + emailIsValid() { + const regex = /.+@/; + + return this.query.match(regex) !== null; }, placeholderText() { if (this.selectedTokens.length === 0) { @@ -54,9 +54,9 @@ export default { this.retrieveUsers(query); }, retrieveUsers: debounce(function debouncedRetrieveUsers() { - return Api.users(this.query, this.$options.queryOptions) - .then(response => { - this.users = response.data.map(token => ({ + return getUsers(this.query, this.$options.queryOptions) + .then((response) => { + this.users = response.data.map((token) => ({ id: token.id, name: token.name, username: token.username, @@ -69,7 +69,7 @@ export default { }); }, USER_SEARCH_DELAY), handleInput() { - this.$emit('input', this.newUsersToInvite); + this.$emit('input', this.selectedTokens); }, handleBlur() { this.hideDropdownWithNoItems = false; @@ -86,6 +86,9 @@ export default { }, }, queryOptions: { exclude_internal: true, active: true }, + i18n: { + inviteTextMessage: __('Invite "%{email}" by email'), + }, }; </script> @@ -94,7 +97,7 @@ export default { v-model="selectedTokens" :dropdown-items="users" :loading="loading" - :allow-user-defined-tokens="false" + :allow-user-defined-tokens="emailIsValid" :hide-dropdown-with-no-items="hideDropdownWithNoItems" :placeholder="placeholderText" :aria-labelledby="ariaLabelledby" @@ -116,5 +119,13 @@ export default { :sub-label="dropdownItem.username" /> </template> + + <template #user-defined-token-content="{ inputText: email }"> + <gl-sprintf :message="$options.i18n.inviteTextMessage"> + <template #email> + <span>{{ email }}</span> + </template> + </gl-sprintf> + </template> </gl-token-selector> </template> diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js index db957ecacfd..74c374018de 100644 --- a/app/assets/javascripts/invite_members/init_invite_members_modal.js +++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import { GlToast } from '@gitlab/ui'; +import { parseBoolean } from '~/lib/utils/common_utils'; import InviteMembersModal from '~/invite_members/components/invite_members_modal.vue'; Vue.use(GlToast); @@ -13,10 +14,11 @@ export default function initInviteMembersModal() { return new Vue({ el, - render: createElement => + render: (createElement) => createElement(InviteMembersModal, { props: { ...el.dataset, + isProject: parseBoolean(el.dataset.isProject), accessLevels: JSON.parse(el.dataset.accessLevels), }, }), diff --git a/app/assets/javascripts/invite_members/init_invite_members_trigger.js b/app/assets/javascripts/invite_members/init_invite_members_trigger.js index bee4f1c0f72..a7b95960995 100644 --- a/app/assets/javascripts/invite_members/init_invite_members_trigger.js +++ b/app/assets/javascripts/invite_members/init_invite_members_trigger.js @@ -10,7 +10,7 @@ export default function initInviteMembersTrigger() { return new Vue({ el, - render: createElement => + render: (createElement) => createElement(InviteMembersTrigger, { props: { ...el.dataset, diff --git a/app/assets/javascripts/issuable/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js index 12f03873958..243d82f55aa 100644 --- a/app/assets/javascripts/issuable/auto_width_dropdown_select.js +++ b/app/assets/javascripts/issuable/auto_width_dropdown_select.js @@ -37,10 +37,7 @@ class AutoWidthDropdownSelect { // We have to look at the parent because // `offsetParent` on a `display: none;` is `null` - const offsetParentWidth = $(this) - .parent() - .offsetParent() - .width(); + const offsetParentWidth = $(this).parent().offsetParent().width(); // Reset any width to let it naturally flow $dropdown.css('width', 'auto'); if ($dropdown.outerWidth(false) > offsetParentWidth) { diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js index 6ba21cd7869..8bb76edbd47 100644 --- a/app/assets/javascripts/issuable_bulk_update_actions.js +++ b/app/assets/javascripts/issuable_bulk_update_actions.js @@ -101,7 +101,7 @@ export default { // Collect unique label IDs for all checked issues this.getElement('.selected-issuable:checked').each((i, el) => { issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'); - issuableLabels.forEach(labelId => { + issuableLabels.forEach((labelId) => { // Store unique IDs if (uniqueIds.indexOf(labelId) === -1) { uniqueIds.push(labelId); @@ -113,7 +113,7 @@ export default { // Add uniqueIds to add it as argument for _.intersection labelIds.unshift(uniqueIds); // Return IDs that are present but not in all selected issueables - return uniqueIds.filter(x => !intersection.apply(this, labelIds).includes(x)); + return uniqueIds.filter((x) => !intersection.apply(this, labelIds).includes(x)); }, getElement(selector) { diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index 7d9cefbe66a..b9daa16874a 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -39,9 +39,9 @@ export default class IssuableBulkUpdateSidebar { } bindEvents() { - this.$bulkUpdateEnableBtn.on('click', e => this.toggleBulkEdit(e, true)); - this.$bulkEditCancelBtn.on('click', e => this.toggleBulkEdit(e, false)); - this.$checkAllContainer.on('click', e => this.selectAll(e)); + this.$bulkUpdateEnableBtn.on('click', (e) => this.toggleBulkEdit(e, true)); + this.$bulkEditCancelBtn.on('click', (e) => this.toggleBulkEdit(e, false)); + this.$checkAllContainer.on('click', (e) => this.selectAll(e)); this.$issuesList.on('change', () => this.updateFormState()); this.$bulkEditSubmitBtn.on('click', () => this.prepForSubmit()); this.$checkAllContainer.on('click', () => this.updateFormState()); @@ -159,7 +159,7 @@ export default class IssuableBulkUpdateSidebar { const $checkedIssues = $('.selected-issuable:checked'); if ($checkedIssues.length > 0) { - return $.map($checkedIssues, value => $(value).data('id')); + return $.map($checkedIssues, (value) => $(value).data('id')); } return []; diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js index 2072e41514d..6fcff90b608 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -31,7 +31,7 @@ export default class IssuableContext { }); $(document) .off('click', '.issuable-sidebar .dropdown-content a') - .on('click', '.issuable-sidebar .dropdown-content a', e => e.preventDefault()); + .on('click', '.issuable-sidebar .dropdown-content a', (e) => e.preventDefault()); $(document) .off('click', '.edit-link') diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 791b5fef699..a8fd7aaecdf 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -52,12 +52,12 @@ export default class IssuableForm { /* eslint-disable @gitlab/require-i18n-strings */ this.wipRegex = new RegExp( '^\\s*(' + // Line start, then any amount of leading whitespace - 'draft\\s-\\s' + // Draft_-_ where "_" are *exactly* one whitespace - '|\\[(draft|wip)\\]\\s*' + // [Draft] or [WIP] and any following whitespace - '|(draft|wip):\\s*' + // Draft: or WIP: and any following whitespace - '|(draft|wip)\\s+' + // Draft_ or WIP_ where "_" is at least one whitespace - '|\\(draft\\)\\s*' + // (Draft) and any following whitespace - ')+' + // At least one repeated match of the preceding parenthetical + 'draft\\s-\\s' + // Draft_-_ where "_" are *exactly* one whitespace + '|\\[(draft|wip)\\]\\s*' + // [Draft] or [WIP] and any following whitespace + '|(draft|wip):\\s*' + // Draft: or WIP: and any following whitespace + '|(draft|wip)\\s+' + // Draft_ or WIP_ where "_" is at least one whitespace + '|\\(draft\\)\\s*' + // (Draft) and any following whitespace + ')+' + // At least one repeated match of the preceding parenthetical '\\s*', // Any amount of trailing whitespace 'i', // Match any case(s) ); @@ -89,9 +89,9 @@ export default class IssuableForm { theme: 'gitlab-theme animate-picker', format: 'yyyy-mm-dd', container: $issuableDueDate.parent().get(0), - parse: dateString => parsePikadayDate(dateString), - toString: date => pikadayToString(date), - onSelect: dateText => $issuableDueDate.val(calendar.toString(dateText)), + parse: (dateString) => parsePikadayDate(dateString), + toString: (date) => pikadayToString(date), + onSelect: (dateText) => $issuableDueDate.val(calendar.toString(dateText)), firstDay: gon.first_day_of_week, }); calendar.setDate(parsePikadayDate($issuableDueDate.val())); @@ -202,7 +202,7 @@ export default class IssuableForm { results(data) { return { // `data` keys are translated so we can't just access them with a string based key - results: data[Object.keys(data)[0]].map(name => ({ + results: data[Object.keys(data)[0]].map((name) => ({ id: name, text: name, })), diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js index e888e481fe5..4f31d26ab5d 100644 --- a/app/assets/javascripts/issuable_index.js +++ b/app/assets/javascripts/issuable_index.js @@ -13,7 +13,7 @@ export default class IssuableIndex { static resetIncomingEmailToken() { const $resetToken = $('.incoming-email-token-reset'); - $resetToken.on('click', e => { + $resetToken.on('click', (e) => { e.preventDefault(); $resetToken.text(s__('EmailToken|resetting...')); @@ -21,9 +21,7 @@ export default class IssuableIndex { axios .put($resetToken.attr('href')) .then(({ data }) => { - $('#issuable_email') - .val(data.new_address) - .focus(); + $('#issuable_email').val(data.new_address).focus(); $resetToken.text(s__('EmailToken|reset it')); }) diff --git a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue index b2312c55f01..c5475a34d3c 100644 --- a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue +++ b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue @@ -200,7 +200,7 @@ export default { this.checkedIssuables[this.issuableId(issuable)].checked = value; }, handleAllIssuablesCheckedInput(value) { - Object.keys(this.checkedIssuables).forEach(issuableId => { + Object.keys(this.checkedIssuables).forEach((issuableId) => { this.checkedIssuables[issuableId].checked = value; }); }, diff --git a/app/assets/javascripts/issuable_show/components/issuable_edit_form.vue b/app/assets/javascripts/issuable_show/components/issuable_edit_form.vue index 93e4db8b99c..830a740ff78 100644 --- a/app/assets/javascripts/issuable_show/components/issuable_edit_form.vue +++ b/app/assets/javascripts/issuable_show/components/issuable_edit_form.vue @@ -141,8 +141,7 @@ export default { :data-supports-quick-actions="enableAutocomplete" :aria-label="__('Description')" :placeholder="__('Write a comment or drag your files hereā¦')" - class="note-textarea js-gfm-input js-autosize markdown-area - qa-description-textarea" + class="note-textarea js-gfm-input js-autosize markdown-area qa-description-textarea" dir="auto" @keydown="handleKeydown($event, 'description')" ></textarea> diff --git a/app/assets/javascripts/issuable_suggestions/components/app.vue b/app/assets/javascripts/issuable_suggestions/components/app.vue index 8a9a880e7ee..ac5f04147d3 100644 --- a/app/assets/javascripts/issuable_suggestions/components/app.vue +++ b/app/assets/javascripts/issuable_suggestions/components/app.vue @@ -29,7 +29,7 @@ export default { skip() { return this.isSearchEmpty; }, - update: data => data.project.issues.edges.map(({ node }) => node), + update: (data) => data.project.issues.edges.map(({ node }) => node), variables() { return { fullPath: this.projectPath, diff --git a/app/assets/javascripts/issuable_suggestions/index.js b/app/assets/javascripts/issuable_suggestions/index.js index 40916c9d27f..9949527106b 100644 --- a/app/assets/javascripts/issuable_suggestions/index.js +++ b/app/assets/javascripts/issuable_suggestions/index.js @@ -5,7 +5,7 @@ import App from './components/app.vue'; Vue.use(VueApollo); -export default function() { +export default function () { const el = document.getElementById('js-suggestions'); const issueTitle = document.getElementById('issue_title'); const { projectPath } = el.dataset; diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 5d2880c3c10..91912c684ad 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -23,7 +23,7 @@ export default class Issue { } // Listen to state changes in the Vue app - document.addEventListener('issuable_vue_app:change', event => { + document.addEventListener('issuable_vue_app:change', (event) => { this.updateTopState(event.detail.isClosed, event.detail.data); }); } @@ -54,11 +54,7 @@ export default class Issue { $(document).trigger('issuable:change', isClosed); let numProjectIssues = Number( - projectIssuesCounter - .first() - .text() - .trim() - .replace(/[^\d]/, ''), + projectIssuesCounter.first().text().trim().replace(/[^\d]/, ''), ); numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1; projectIssuesCounter.text(addDelimiter(numProjectIssues)); @@ -84,7 +80,7 @@ export default class Issue { alertMovedFromServiceDeskWarning.show(); } - alertMovedFromServiceDeskWarning.on('click', '.js-close', e => { + alertMovedFromServiceDeskWarning.on('click', '.js-close', (e) => { e.preventDefault(); e.stopImmediatePropagation(); alertMovedFromServiceDeskWarning.remove(); diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index 61e5db0970a..d569ad573a2 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -250,7 +250,7 @@ export default { this.poll = new Poll({ resource: this.service, method: 'getData', - successCallback: res => this.store.updateState(res.data), + successCallback: (res) => this.store.updateState(res.data), errorCallback(err) { throw new Error(err); }, @@ -294,8 +294,8 @@ export default { updateStoreState() { return this.service .getData() - .then(res => res.data) - .then(data => { + .then((res) => res.data) + .then((data) => { this.store.updateState(data); }) .catch(() => { @@ -320,7 +320,7 @@ export default { requestTemplatesAndShowForm() { return this.service .loadTemplates(this.issuableTemplateNamesPath) - .then(res => { + .then((res) => { this.updateAndShowForm(res.data); }) .catch(() => { @@ -345,9 +345,9 @@ export default { updateIssuable() { return this.service .updateIssuable(this.store.formState) - .then(res => res.data) - .then(data => this.checkForSpam(data)) - .then(data => { + .then((res) => res.data) + .then((data) => this.checkForSpam(data)) + .then((data) => { if (!window.location.pathname.includes(data.web_url)) { visitUrl(data.web_url); } @@ -384,8 +384,8 @@ export default { deleteIssuable(payload) { return this.service .deleteIssuable(payload) - .then(res => res.data) - .then(data => { + .then((res) => res.data) + .then((data) => { // Stop the poll so we don't get 404's with the issuable not existing this.poll.stop(); diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue index 0de0060615b..8d417e32d62 100644 --- a/app/assets/javascripts/issue_show/components/fields/description.vue +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -1,12 +1,13 @@ <script> import updateMixin from '../../mixins/update'; -import markdownField from '../../../vue_shared/components/markdown/field.vue'; +import markdownField from '~/vue_shared/components/markdown/field.vue'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { components: { markdownField, }, - mixins: [updateMixin], + mixins: [glFeatureFlagsMixin(), updateMixin], props: { formState: { type: Object, @@ -52,10 +53,9 @@ export default { id="issue-description" ref="textarea" v-model="formState.description" - class="note-textarea js-gfm-input js-autosize markdown-area - qa-description-textarea" + class="note-textarea js-gfm-input js-autosize markdown-area qa-description-textarea" dir="auto" - data-supports-quick-actions="true" + :data-supports-quick-actions="!glFeatures.tributeAutocomplete" :aria-label="__('Description')" :placeholder="__('Write a comment or drag your files hereā¦')" @keydown.meta.enter="updateIssuable" diff --git a/app/assets/javascripts/issue_show/components/fields/description_template.vue b/app/assets/javascripts/issue_show/components/fields/description_template.vue index ea6e03404e7..71299381aae 100644 --- a/app/assets/javascripts/issue_show/components/fields/description_template.vue +++ b/app/assets/javascripts/issue_show/components/fields/description_template.vue @@ -34,7 +34,7 @@ export default { mounted() { // Create the editor for the template const editor = document.querySelector('.detail-page-description .note-textarea') || {}; - editor.setValue = val => { + editor.setValue = (val) => { this.formState.description = val; }; editor.getValue = () => this.formState.description; diff --git a/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue b/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue index c593fa33973..f9f06c3ad5a 100644 --- a/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue +++ b/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue @@ -6,7 +6,6 @@ import createFlash from '~/flash'; import { s__ } from '~/locale'; import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; import Tracking from '~/tracking'; - import getAlert from './graphql/queries/get_alert.graphql'; import { trackIncidentDetailsViewsOptions } from '~/incidents/constants'; @@ -17,8 +16,9 @@ export default { GlTab, GlTabs, HighlightBar, + MetricsTab: () => import('ee_component/issue_show/components/incidents/metrics_tab.vue'), }, - inject: ['fullPath', 'iid'], + inject: ['fullPath', 'iid', 'uploadMetricsFeatureAvailable'], apollo: { alert: { query: getAlert, @@ -67,7 +67,13 @@ export default { <highlight-bar :alert="alert" /> <description-component v-bind="$attrs" /> </gl-tab> - <gl-tab v-if="alert" class="alert-management-details" :title="s__('Incident|Alert details')"> + <metrics-tab v-if="uploadMetricsFeatureAvailable" data-testid="metrics-tab" /> + <gl-tab + v-if="alert" + class="alert-management-details" + :title="s__('Incident|Alert details')" + data-testid="alert-details-tab" + > <alert-details-table :alert="alert" :loading="loading" /> </gl-tab> </gl-tabs> diff --git a/app/assets/javascripts/issue_show/incident.js b/app/assets/javascripts/issue_show/incident.js index 618fb551f28..ccac38811b5 100644 --- a/app/assets/javascripts/issue_show/incident.js +++ b/app/assets/javascripts/issue_show/incident.js @@ -12,7 +12,17 @@ export default function initIssuableApp(issuableData = {}) { defaultClient: createDefaultClient(), }); - const { iid, projectNamespace, projectPath, slaFeatureAvailable } = issuableData; + const { + canUpdate, + iid, + projectNamespace, + projectPath, + projectId, + slaFeatureAvailable, + uploadMetricsFeatureAvailable, + } = issuableData; + + const fullPath = `${projectNamespace}/${projectPath}`; return new Vue({ el: document.getElementById('js-issuable-app'), @@ -21,9 +31,12 @@ export default function initIssuableApp(issuableData = {}) { issuableApp, }, provide: { - fullPath: `${projectNamespace}/${projectPath}`, + canUpdate, + fullPath, iid, + projectId, slaFeatureAvailable: parseBoolean(slaFeatureAvailable), + uploadMetricsFeatureAvailable: parseBoolean(uploadMetricsFeatureAvailable), }, render(createElement) { return createElement('issuable-app', { diff --git a/app/assets/javascripts/issue_show/issue.js b/app/assets/javascripts/issue_show/issue.js index 8260460828b..83fd1355f26 100644 --- a/app/assets/javascripts/issue_show/issue.js +++ b/app/assets/javascripts/issue_show/issue.js @@ -57,6 +57,6 @@ export function initIssueHeaderActions(store) { reportAbusePath: el.dataset.reportAbusePath, submitAsSpamPath: el.dataset.submitAsSpamPath, }, - render: createElement => createElement(HeaderActions), + render: (createElement) => createElement(HeaderActions), }); } diff --git a/app/assets/javascripts/issue_show/utils/parse_data.js b/app/assets/javascripts/issue_show/utils/parse_data.js index 12f38005366..d5e7d2a8807 100644 --- a/app/assets/javascripts/issue_show/utils/parse_data.js +++ b/app/assets/javascripts/issue_show/utils/parse_data.js @@ -4,7 +4,7 @@ import { sanitize } from '~/lib/dompurify'; // We currently load + parse the data from the issue app and related merge request let cachedParsedData; -export const parseIssuableData = el => { +export const parseIssuableData = (el) => { try { if (cachedParsedData) return cachedParsedData; diff --git a/app/assets/javascripts/issues_list/components/issuable.vue b/app/assets/javascripts/issues_list/components/issuable.vue index 16f8e67cde0..3965fd6b0c7 100644 --- a/app/assets/javascripts/issues_list/components/issuable.vue +++ b/app/assets/javascripts/issues_list/components/issuable.vue @@ -37,7 +37,6 @@ export default { openedAgoJira: __('opened %{timeAgoString} by %{user} in Jira'), openedAgoServiceDesk: __('opened %{timeAgoString} by %{email} via %{user}'), }, - inject: ['scopedLabelsAvailable'], components: { IssueAssignees, GlLink, @@ -51,6 +50,7 @@ export default { GlTooltip, SafeHtml, }, + inject: ['scopedLabelsAvailable'], props: { issuable: { type: Object, @@ -110,7 +110,7 @@ export default { return getDayDifference(new Date(this.issuable.created_at), new Date()) < 1; }, labelIdsString() { - return JSON.stringify(this.issuable.labels.map(l => l.id)); + return JSON.stringify(this.issuable.labels.map((l) => l.id)); }, milestoneDueDate() { const { due_date: dueDate } = this.issuable.milestone || {}; diff --git a/app/assets/javascripts/issues_list/components/issuables_list_app.vue b/app/assets/javascripts/issues_list/components/issuables_list_app.vue index 0ce2bcc1cce..eda8bc2b61f 100644 --- a/app/assets/javascripts/issues_list/components/issuables_list_app.vue +++ b/app/assets/javascripts/issues_list/components/issuables_list_app.vue @@ -208,7 +208,7 @@ export default { }, mounted() { if (this.canBulkEdit) { - this.unsubscribeToggleBulkEdit = issueableEventHub.$on('issuables:toggleBulkEdit', val => { + this.unsubscribeToggleBulkEdit = issueableEventHub.$on('issuables:toggleBulkEdit', (val) => { this.isBulkEditing = val; }); } @@ -223,7 +223,7 @@ export default { return Boolean(this.selection[issuableId]); }, setSelection(ids) { - ids.forEach(id => { + ids.forEach((id) => { this.select(id, true); }); }, @@ -254,7 +254,7 @@ export default { per_page: this.itemsPerPage, }, }) - .then(response => { + .then((response) => { this.loading = false; this.issuables = response.data; this.totalItems = Number(response.headers['x-total']); @@ -335,7 +335,7 @@ export default { handleFilter(filters) { let search = null; - filters.forEach(filter => { + filters.forEach((filter) => { if (typeof filter === 'string') { search = filter; } diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues_list/index.js index 5ef86536865..42e97766b95 100644 --- a/app/assets/javascripts/issues_list/index.js +++ b/app/assets/javascripts/issues_list/index.js @@ -40,7 +40,7 @@ function mountIssuablesListApp() { return; } - document.querySelectorAll('.js-issuables-list').forEach(el => { + document.querySelectorAll('.js-issuables-list').forEach((el) => { const { canBulkEdit, emptyStateMeta = {}, scopedLabelsAvailable, ...data } = el.dataset; return new Vue({ diff --git a/app/assets/javascripts/jira_connect/api.js b/app/assets/javascripts/jira_connect/api.js new file mode 100644 index 00000000000..d689a2d1962 --- /dev/null +++ b/app/assets/javascripts/jira_connect/api.js @@ -0,0 +1,33 @@ +import axios from 'axios'; + +const getJwt = async () => { + return AP.context.getToken(); +}; + +export const addSubscription = async (addPath, namespace) => { + const jwt = await getJwt(); + + return axios.post(addPath, { + jwt, + namespace_path: namespace, + }); +}; + +export const removeSubscription = async (removePath) => { + const jwt = await getJwt(); + + return axios.delete(removePath, { + params: { + jwt, + }, + }); +}; + +export const fetchGroups = async (groupsPath, { page, perPage }) => { + return axios.get(groupsPath, { + params: { + page, + per_page: perPage, + }, + }); +}; diff --git a/app/assets/javascripts/jira_connect/components/app.vue b/app/assets/javascripts/jira_connect/components/app.vue index 490bf2fdd66..f5bf30f4488 100644 --- a/app/assets/javascripts/jira_connect/components/app.vue +++ b/app/assets/javascripts/jira_connect/components/app.vue @@ -1,16 +1,63 @@ <script> +import { mapState } from 'vuex'; +import { GlAlert, GlButton, GlModal, GlModalDirective } from '@gitlab/ui'; +import { __ } from '~/locale'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import GroupsList from './groups_list.vue'; + export default { name: 'JiraConnectApp', + components: { + GlAlert, + GlButton, + GlModal, + GroupsList, + }, + directives: { + GlModalDirective, + }, + mixins: [glFeatureFlagsMixin()], computed: { - state() { - return this.$root.$data.state || {}; + ...mapState(['errorMessage']), + showNewUI() { + return this.glFeatures.newJiraConnectUi; }, - error() { - return this.state.error; + }, + modal: { + cancelProps: { + text: __('Cancel'), }, }, }; </script> + <template> - <div></div> + <div> + <gl-alert v-if="errorMessage" class="gl-mb-6" variant="danger" :dismissible="false"> + {{ errorMessage }} + </gl-alert> + + <h1>GitLab for Jira Configuration</h1> + + <div + v-if="showNewUI" + class="gl-display-flex gl-justify-content-space-between gl-my-5 gl-pb-4 gl-border-b-solid gl-border-b-1 gl-border-b-gray-200" + > + <h3 data-testid="new-jira-connect-ui-heading">{{ s__('Integrations|Linked namespaces') }}</h3> + <gl-button + v-gl-modal-directive="'add-namespace-modal'" + category="primary" + variant="info" + class="gl-align-self-center" + >{{ s__('Integrations|Add namespace') }}</gl-button + > + <gl-modal + modal-id="add-namespace-modal" + :title="s__('Integrations|Link namespaces')" + :action-cancel="$options.modal.cancelProps" + > + <groups-list /> + </gl-modal> + </div> + </div> </template> diff --git a/app/assets/javascripts/jira_connect/components/groups_list.vue b/app/assets/javascripts/jira_connect/components/groups_list.vue new file mode 100644 index 00000000000..eeddd32addc --- /dev/null +++ b/app/assets/javascripts/jira_connect/components/groups_list.vue @@ -0,0 +1,88 @@ +<script> +import { GlTabs, GlTab, GlLoadingIcon, GlPagination } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; +import { fetchGroups } from '~/jira_connect/api'; +import { defaultPerPage } from '~/jira_connect/constants'; +import GroupsListItem from './groups_list_item.vue'; + +export default { + components: { + GlTabs, + GlTab, + GlLoadingIcon, + GlPagination, + GroupsListItem, + }, + inject: { + groupsPath: { + default: '', + }, + }, + data() { + return { + groups: [], + isLoading: false, + page: 1, + perPage: defaultPerPage, + totalItems: 0, + }; + }, + mounted() { + this.loadGroups(); + }, + methods: { + loadGroups() { + this.isLoading = true; + + fetchGroups(this.groupsPath, { + page: this.page, + perPage: this.perPage, + }) + .then((response) => { + const { page, total } = parseIntPagination(normalizeHeaders(response.headers)); + this.page = page; + this.totalItems = total; + this.groups = response.data; + }) + .catch(() => { + // eslint-disable-next-line no-alert + alert(s__('Integrations|Failed to load namespaces. Please try again.')); + }) + .finally(() => { + this.isLoading = false; + }); + }, + }, +}; +</script> + +<template> + <gl-tabs> + <gl-tab :title="__('Groups and subgroups')" class="gl-pt-3"> + <gl-loading-icon v-if="isLoading" size="md" /> + <div v-else-if="groups.length === 0" class="gl-text-center"> + <h5>{{ s__('Integrations|No available namespaces.') }}</h5> + <p class="gl-mt-5"> + {{ + s__('Integrations|You must have owner or maintainer permissions to link namespaces.') + }} + </p> + </div> + <ul v-else class="gl-list-style-none gl-pl-0"> + <groups-list-item v-for="group in groups" :key="group.id" :group="group" /> + </ul> + + <div class="gl-display-flex gl-justify-content-center gl-mt-5"> + <gl-pagination + v-if="totalItems > perPage && groups.length > 0" + v-model="page" + class="gl-mb-0" + :per-page="perPage" + :total-items="totalItems" + @input="loadGroups" + /> + </div> + </gl-tab> + </gl-tabs> +</template> diff --git a/app/assets/javascripts/jira_connect/components/groups_list_item.vue b/app/assets/javascripts/jira_connect/components/groups_list_item.vue new file mode 100644 index 00000000000..15e37ab3cb0 --- /dev/null +++ b/app/assets/javascripts/jira_connect/components/groups_list_item.vue @@ -0,0 +1,42 @@ +<script> +import { GlIcon, GlAvatar } from '@gitlab/ui'; + +export default { + components: { + GlIcon, + GlAvatar, + }, + props: { + group: { + type: Object, + required: true, + }, + }, +}; +</script> + +<template> + <li class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-200"> + <div class="gl-display-flex gl-align-items-center gl-py-3"> + <gl-icon name="folder-o" class="gl-mr-3" /> + <div class="gl-display-none gl-flex-shrink-0 gl-display-sm-flex gl-mr-3"> + <gl-avatar :size="32" shape="rect" :entity-name="group.name" :src="group.avatar_url" /> + </div> + <div class="gl-min-w-0 gl-display-flex gl-flex-grow-1 gl-flex-shrink-1 gl-align-items-center"> + <div class="gl-min-w-0 gl-flex-grow-1 flex-shrink-1"> + <div class="gl-display-flex gl-align-items-center gl-flex-wrap"> + <span + class="gl-mr-3 gl-text-gray-900! gl-font-weight-bold" + data-testid="group-list-item-name" + > + {{ group.full_name }} + </span> + </div> + <div v-if="group.description" data-testid="group-list-item-description"> + <p class="gl-mt-2! gl-mb-0 gl-text-gray-600" v-text="group.description"></p> + </div> + </div> + </div> + </div> + </li> +</template> diff --git a/app/assets/javascripts/jira_connect/constants.js b/app/assets/javascripts/jira_connect/constants.js new file mode 100644 index 00000000000..2b3be5cd5cd --- /dev/null +++ b/app/assets/javascripts/jira_connect/constants.js @@ -0,0 +1 @@ +export const defaultPerPage = 10; diff --git a/app/assets/javascripts/jira_connect/index.js b/app/assets/javascripts/jira_connect/index.js index e7aa4c437bb..dc2a77f4e0c 100644 --- a/app/assets/javascripts/jira_connect/index.js +++ b/app/assets/javascripts/jira_connect/index.js @@ -1,18 +1,21 @@ import Vue from 'vue'; +import Vuex from 'vuex'; import $ from 'jquery'; -import App from './components/app.vue'; - -const store = { - state: { - error: '', - }, - setErrorMessage(errorMessage) { - this.state.error = errorMessage; - }, -}; +import setConfigs from '@gitlab/ui/dist/config'; +import Translate from '~/vue_shared/translate'; +import GlFeatureFlagsPlugin from '~/vue_shared/gl_feature_flags_plugin'; + +import JiraConnectApp from './components/app.vue'; +import { addSubscription, removeSubscription } from '~/jira_connect/api'; +import createStore from './store'; +import { SET_ERROR_MESSAGE } from './store/mutation_types'; + +Vue.use(Vuex); + +const store = createStore(); /** - * Initialize necessary form handlers for the Jira Connect app + * Initialize form handlers for the Jira Connect app */ const initJiraFormHandlers = () => { const reqComplete = () => { @@ -20,53 +23,40 @@ const initJiraFormHandlers = () => { }; const reqFailed = (res, fallbackErrorMessage) => { - const { responseJSON: { error = fallbackErrorMessage } = {} } = res || {}; + const { error = fallbackErrorMessage } = res || {}; - store.setErrorMessage(error); - // eslint-disable-next-line no-alert - alert(error); + store.commit(SET_ERROR_MESSAGE, error); }; - AP.getLocation(location => { - $('.js-jira-connect-sign-in').each(function updateSignInLink() { - const updatedLink = `${$(this).attr('href')}?return_to=${location}`; - $(this).attr('href', updatedLink); + if (typeof AP.getLocation === 'function') { + AP.getLocation((location) => { + $('.js-jira-connect-sign-in').each(function updateSignInLink() { + const updatedLink = `${$(this).attr('href')}?return_to=${location}`; + $(this).attr('href', updatedLink); + }); }); - }); + } $('#add-subscription-form').on('submit', function onAddSubscriptionForm(e) { - const actionUrl = $(this).attr('action'); + const addPath = $(this).attr('action'); + const namespace = $('#namespace-input').val(); + e.preventDefault(); - AP.context.getToken(token => { - // eslint-disable-next-line no-jquery/no-ajax - $.post(actionUrl, { - jwt: token, - namespace_path: $('#namespace-input').val(), - format: 'json', - }) - .done(reqComplete) - .fail(err => reqFailed(err, 'Failed to add namespace. Please try again.')); - }); + addSubscription(addPath, namespace) + .then(reqComplete) + .catch((err) => reqFailed(err.response.data, 'Failed to add namespace. Please try again.')); }); $('.remove-subscription').on('click', function onRemoveSubscriptionClick(e) { - const href = $(this).attr('href'); + const removePath = $(this).attr('href'); e.preventDefault(); - AP.context.getToken(token => { - // eslint-disable-next-line no-jquery/no-ajax - $.ajax({ - url: href, - method: 'DELETE', - data: { - jwt: token, - format: 'json', - }, - }) - .done(reqComplete) - .fail(err => reqFailed(err, 'Failed to remove namespace. Please try again.')); - }); + removeSubscription(removePath) + .then(reqComplete) + .catch((err) => + reqFailed(err.response.data, 'Failed to remove namespace. Please try again.'), + ); }); }; @@ -75,13 +65,24 @@ function initJiraConnect() { initJiraFormHandlers(); + if (!el) { + return null; + } + + setConfigs(); + Vue.use(Translate); + Vue.use(GlFeatureFlagsPlugin); + + const { groupsPath } = el.dataset; + return new Vue({ el, - data: { - state: store.state, + store, + provide: { + groupsPath, }, render(createElement) { - return createElement(App, {}); + return createElement(JiraConnectApp); }, }); } diff --git a/app/assets/javascripts/jira_connect/store/index.js b/app/assets/javascripts/jira_connect/store/index.js new file mode 100644 index 00000000000..aa7e14269a4 --- /dev/null +++ b/app/assets/javascripts/jira_connect/store/index.js @@ -0,0 +1,9 @@ +import Vuex from 'vuex'; +import mutations from './mutations'; +import state from './state'; + +export default () => + new Vuex.Store({ + state, + mutations, + }); diff --git a/app/assets/javascripts/jira_connect/store/mutation_types.js b/app/assets/javascripts/jira_connect/store/mutation_types.js new file mode 100644 index 00000000000..7f6ff1256bb --- /dev/null +++ b/app/assets/javascripts/jira_connect/store/mutation_types.js @@ -0,0 +1 @@ +export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE'; diff --git a/app/assets/javascripts/jira_connect/store/mutations.js b/app/assets/javascripts/jira_connect/store/mutations.js new file mode 100644 index 00000000000..c3acd07f89f --- /dev/null +++ b/app/assets/javascripts/jira_connect/store/mutations.js @@ -0,0 +1,7 @@ +import { SET_ERROR_MESSAGE } from './mutation_types'; + +export default { + [SET_ERROR_MESSAGE](state, errorMessage) { + state.errorMessage = errorMessage; + }, +}; diff --git a/app/assets/javascripts/jira_connect/store/state.js b/app/assets/javascripts/jira_connect/store/state.js new file mode 100644 index 00000000000..079b8350770 --- /dev/null +++ b/app/assets/javascripts/jira_connect/store/state.js @@ -0,0 +1,3 @@ +export default () => ({ + errorMessage: undefined, +}); diff --git a/app/assets/javascripts/jira_import/components/jira_import_form.vue b/app/assets/javascripts/jira_import/components/jira_import_form.vue index 4a1bca110fd..ab475c3c85a 100644 --- a/app/assets/javascripts/jira_import/components/jira_import_form.vue +++ b/app/assets/javascripts/jira_import/components/jira_import_form.vue @@ -118,7 +118,7 @@ export default { this.getJiraUserMapping(); this.searchUsers() - .then(data => { + .then((data) => { this.initialUsers = data; }) .catch(() => {}); @@ -219,7 +219,7 @@ export default { } }, updateMapping(jiraAccountId, gitlabId, gitlabUsername) { - this.userMappings = this.userMappings.map(userMapping => + this.userMappings = this.userMappings.map((userMapping) => userMapping.jiraAccountId === jiraAccountId ? { ...userMapping, diff --git a/app/assets/javascripts/jira_import/utils/cache_update.js b/app/assets/javascripts/jira_import/utils/cache_update.js index 65b2e459f03..db7dbb7353f 100644 --- a/app/assets/javascripts/jira_import/utils/cache_update.js +++ b/app/assets/javascripts/jira_import/utils/cache_update.js @@ -20,7 +20,7 @@ export const addInProgressImportToStore = (store, jiraImportStart, fullPath) => store.writeQuery({ ...queryDetails, - data: produce(sourceData, draftData => { + data: produce(sourceData, (draftData) => { draftData.project.jiraImportStatus = IMPORT_STATE.SCHEDULED; // eslint-disable-line no-param-reassign // eslint-disable-next-line no-param-reassign draftData.project.jiraImports.nodes = [ diff --git a/app/assets/javascripts/jira_import/utils/jira_import_utils.js b/app/assets/javascripts/jira_import/utils/jira_import_utils.js index edd6fad4aac..4e3b5b2fbde 100644 --- a/app/assets/javascripts/jira_import/utils/jira_import_utils.js +++ b/app/assets/javascripts/jira_import/utils/jira_import_utils.js @@ -9,10 +9,10 @@ export const IMPORT_STATE = { STARTED: 'started', }; -export const isInProgress = state => +export const isInProgress = (state) => state === IMPORT_STATE.SCHEDULED || state === IMPORT_STATE.STARTED; -export const isFinished = state => state === IMPORT_STATE.FINISHED; +export const isFinished = (state) => state === IMPORT_STATE.FINISHED; /** * Converts the list of Jira projects into a format consumable by GlFormSelect. @@ -22,7 +22,7 @@ export const isFinished = state => state === IMPORT_STATE.FINISHED; * @param {string} projects[].name - Jira project name * @returns {Object[]} - List of Jira projects in a format consumable by GlFormSelect */ -export const extractJiraProjectsOptions = projects => +export const extractJiraProjectsOptions = (projects) => projects.map(({ key, name }) => ({ text: `${name} (${key})`, value: key })); /** @@ -32,10 +32,10 @@ export const extractJiraProjectsOptions = projects => * @param {string} jiraImports[].jiraProjectKey - Jira project key * @returns {string} - A label title */ -const calculateJiraImportLabelTitle = jiraImports => { +const calculateJiraImportLabelTitle = (jiraImports) => { const mostRecentJiraProjectKey = last(jiraImports)?.jiraProjectKey; const jiraProjectImportCount = jiraImports.filter( - jiraImport => jiraImport.jiraProjectKey === mostRecentJiraProjectKey, + (jiraImport) => jiraImport.jiraProjectKey === mostRecentJiraProjectKey, ).length; return `jira-import::${mostRecentJiraProjectKey}-${jiraProjectImportCount}`; }; @@ -50,7 +50,7 @@ const calculateJiraImportLabelTitle = jiraImports => { * @returns {string} - The label color associated with the given labelTitle */ const calculateJiraImportLabelColor = (labelTitle, labels) => - labels.find(label => label.title === labelTitle)?.color; + labels.find((label) => label.title === labelTitle)?.color; /** * Calculates the label for the most recent Jira import. @@ -91,7 +91,7 @@ export const shouldShowFinishedAlert = (labelTitle, importStatus) => { * * @param {string} labelTitle - Jira import label, for checking localStorage */ -export const setFinishedAlertHideMap = labelTitle => { +export const setFinishedAlertHideMap = (labelTitle) => { const finishedAlertHideMap = JSON.parse(localStorage.getItem(JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY)) || {}; diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index 30093224631..b0ba6ce52d1 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -134,7 +134,7 @@ export default { if (isEmpty(oldVal) && !isEmpty(newVal.pipeline)) { const stages = this.job.pipeline.details.stages || []; - const defaultStage = stages.find(stage => stage && stage.name === this.selectedStage); + const defaultStage = stages.find((stage) => stage && stage.name === this.selectedStage); if (defaultStage) { this.fetchJobsForStage(defaultStage); diff --git a/app/assets/javascripts/jobs/components/job_retry_forward_deployment_modal.vue b/app/assets/javascripts/jobs/components/job_retry_forward_deployment_modal.vue index 5ce9d08035d..e83ed6c6332 100644 --- a/app/assets/javascripts/jobs/components/job_retry_forward_deployment_modal.vue +++ b/app/assets/javascripts/jobs/components/job_retry_forward_deployment_modal.vue @@ -11,6 +11,11 @@ export default { i18n: { ...JOB_RETRY_FORWARD_DEPLOYMENT_MODAL, }, + inject: { + retryOutdatedJobDocsUrl: { + default: '', + }, + }, props: { modalId: { type: String, @@ -21,11 +26,6 @@ export default { required: true, }, }, - inject: { - retryOutdatedJobDocsUrl: { - default: '', - }, - }, data() { return { primaryProps: { diff --git a/app/assets/javascripts/jobs/components/log/line.vue b/app/assets/javascripts/jobs/components/log/line.vue index 87af387ca91..2d9714cd06b 100644 --- a/app/assets/javascripts/jobs/components/log/line.vue +++ b/app/assets/javascripts/jobs/components/log/line.vue @@ -18,7 +18,7 @@ export default { render(h, { props }) { const { line, path } = props; - const chars = line.content.map(content => { + const chars = line.content.map((content) => { return h( 'span', { @@ -26,7 +26,7 @@ export default { }, // Simple "tokenization": Split text in chunks of text // which alternate between text and urls. - content.text.split(linkRegex).map(chunk => { + content.text.split(linkRegex).map((chunk) => { // Return normal string for non-links if (!chunk.match(linkRegex)) { return chunk; diff --git a/app/assets/javascripts/jobs/components/manual_variables_form.vue b/app/assets/javascripts/jobs/components/manual_variables_form.vue index bf1930c9a37..24276cbe60a 100644 --- a/app/assets/javascripts/jobs/components/manual_variables_form.vue +++ b/app/assets/javascripts/jobs/components/manual_variables_form.vue @@ -93,7 +93,10 @@ export default { this.secretValue = ''; }, deleteVariable(id) { - this.variables.splice(this.variables.findIndex(el => el.id === id), 1); + this.variables.splice( + this.variables.findIndex((el) => el.id === id), + 1, + ); }, }, }; diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue index aeae9f26ed3..7d4fe0a0680 100644 --- a/app/assets/javascripts/jobs/components/stages_dropdown.vue +++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue @@ -48,7 +48,10 @@ export default { <ci-icon :status="pipeline.details.status" class="vertical-align-middle" /> <span class="font-weight-bold">{{ s__('Job|Pipeline') }}</span> - <gl-link :href="pipeline.path" class="js-pipeline-path link-commit qa-pipeline-path" + <gl-link + :href="pipeline.path" + class="js-pipeline-path link-commit" + data-qa-selector="pipeline_path" >#{{ pipeline.id }}</gl-link > <template v-if="hasRef"> diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/trigger_block.vue index 3cb5e63fd36..1d46dd8cea4 100644 --- a/app/assets/javascripts/jobs/components/trigger_block.vue +++ b/app/assets/javascripts/jobs/components/trigger_block.vue @@ -27,7 +27,7 @@ export default { return this.showVariableValues ? __('Hide values') : __('Reveal values'); }, hasValues() { - return this.trigger.variables.some(v => v.value); + return this.trigger.variables.some((v) => v.value); }, }, methods: { diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js index e3ded725168..e76a3693db9 100644 --- a/app/assets/javascripts/jobs/store/actions.js +++ b/app/assets/javascripts/jobs/store/actions.js @@ -2,7 +2,7 @@ import Visibility from 'visibilityjs'; import * as types from './mutation_types'; import axios from '~/lib/utils/axios_utils'; import Poll from '~/lib/utils/poll'; -import { setFaviconOverlay, resetFavicon } from '~/lib/utils/common_utils'; +import { setFaviconOverlay, resetFavicon } from '~/lib/utils/favicon'; import { deprecatedCreateFlash as flash } from '~/flash'; import { __ } from '~/locale'; import { @@ -173,7 +173,7 @@ export const fetchTrace = ({ dispatch, state }) => dispatch('startPollingTrace'); } }) - .catch(e => + .catch((e) => e.response.status === httpStatusCodes.FORBIDDEN ? dispatch('receiveTraceUnauthorizedError') : dispatch('receiveTraceError'), @@ -229,7 +229,7 @@ export const fetchJobsForStage = ({ dispatch }, stage = {}) => { }, }) .then(({ data }) => { - const retriedJobs = data.retried.map(job => ({ ...job, retried: true })); + const retriedJobs = data.retried.map((job) => ({ ...job, retried: true })); const jobs = data.latest_statuses.concat(retriedJobs); dispatch('receiveJobsForStageSuccess', jobs); @@ -244,7 +244,7 @@ export const receiveJobsForStageError = ({ commit }) => { }; export const triggerManualJob = ({ state }, variables) => { - const parsedVariables = variables.map(variable => { + const parsedVariables = variables.map((variable) => { const copyVar = { ...variable }; delete copyVar.id; return copyVar; diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js index 8c2d1dd8ab2..30a4a247dc4 100644 --- a/app/assets/javascripts/jobs/store/getters.js +++ b/app/assets/javascripts/jobs/store/getters.js @@ -1,37 +1,37 @@ import { isEmpty, isString } from 'lodash'; import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; -export const headerTime = state => (state.job.started ? state.job.started : state.job.created_at); +export const headerTime = (state) => (state.job.started ? state.job.started : state.job.created_at); -export const hasForwardDeploymentFailure = state => +export const hasForwardDeploymentFailure = (state) => state?.job?.failure_reason === 'forward_deployment_failure'; -export const hasUnmetPrerequisitesFailure = state => +export const hasUnmetPrerequisitesFailure = (state) => state?.job?.failure_reason === 'unmet_prerequisites'; -export const shouldRenderCalloutMessage = state => +export const shouldRenderCalloutMessage = (state) => !isEmpty(state.job.status) && !isEmpty(state.job.callout_message); /** * When job has not started the key will be null * When job started the key will be a string with a date. */ -export const shouldRenderTriggeredLabel = state => isString(state.job.started); +export const shouldRenderTriggeredLabel = (state) => isString(state.job.started); -export const hasEnvironment = state => !isEmpty(state.job.deployment_status); +export const hasEnvironment = (state) => !isEmpty(state.job.deployment_status); /** * Checks if it the job has trace. * Used to check if it should render the job log or the empty state * @returns {Boolean} */ -export const hasTrace = state => +export const hasTrace = (state) => state.job.has_trace || (!isEmpty(state.job.status) && state.job.status.group === 'running'); -export const emptyStateIllustration = state => +export const emptyStateIllustration = (state) => (state.job && state.job.status && state.job.status.illustration) || {}; -export const emptyStateAction = state => +export const emptyStateAction = (state) => (state.job && state.job.status && state.job.status.action) || null; /** @@ -40,12 +40,12 @@ export const emptyStateAction = state => * * @returns {Boolean} */ -export const shouldRenderSharedRunnerLimitWarning = state => +export const shouldRenderSharedRunnerLimitWarning = (state) => !isEmpty(state.job.runners) && !isEmpty(state.job.runners.quota) && state.job.runners.quota.used >= state.job.runners.quota.limit; -export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete; +export const isScrollingDown = (state) => isScrolledToBottom() && !state.isTraceComplete; -export const hasRunnersForProject = state => +export const hasRunnersForProject = (state) => state.job.runners.available && !state.job.runners.online; diff --git a/app/assets/javascripts/jobs/store/utils.js b/app/assets/javascripts/jobs/store/utils.js index ea9c214de32..a0e0a0fb8bd 100644 --- a/app/assets/javascripts/jobs/store/utils.js +++ b/app/assets/javascripts/jobs/store/utils.js @@ -43,7 +43,7 @@ export const parseHeaderLine = (line = {}, lineNumber) => ({ * @param Object durationLine */ export function addDurationToHeader(data, durationLine) { - data.forEach(el => { + data.forEach((el) => { if (el.line && el.line.section === durationLine.section) { el.line.section_duration = durationLine.section_duration; } @@ -72,7 +72,7 @@ export const isCollapsibleSection = (acc = [], last = {}, section = {}) => * @param Array acc * @returns Number */ -export const getIncrementalLineNumber = acc => { +export const getIncrementalLineNumber = (acc) => { let lineNumberValue; const lastIndex = acc.length - 1; const lastElement = acc[lastIndex]; diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js index 469f7ce94b0..92dffb87e1a 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -135,7 +135,7 @@ export default class LabelManager { getSortedLabelsIds() { const sortedIds = []; - this.prioritizedLabels.find('> li').each(function() { + this.prioritizedLabels.find('> li').each(function () { const id = $(this).data('id'); if (id) { diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index ac5aa24d5d8..337d063b02a 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -49,7 +49,7 @@ export default class LabelsSelect { const fieldName = $dropdown.data('fieldName'); let initialSelected = $selectbox .find(`input[name="${$dropdown.data('fieldName')}"]`) - .map(function() { + .map(function () { return this.value; }) .get(); @@ -64,11 +64,11 @@ export default class LabelsSelect { ); } - const saveLabelData = function() { + const saveLabelData = function () { const selected = $dropdown .closest('.selectbox') .find(`input[name='${fieldName}']`) - .map(function() { + .map(function () { return this.value; }) .get(); @@ -124,15 +124,15 @@ export default class LabelsSelect { const toRemoveIds = Array.from( $form.find(`input[type="hidden"][name="${fieldName}"]`), ) - .map(el => el.value) + .map((el) => el.value) .map(Number); - data.labels.forEach(label => { + data.labels.forEach((label) => { const index = toRemoveIds.indexOf(label.id); toRemoveIds.splice(index, 1); }); - toRemoveIds.forEach(id => { + toRemoveIds.forEach((id) => { $form .find(`input[type="hidden"][name="${fieldName}"][value="${id}"]`) .last() @@ -157,7 +157,7 @@ export default class LabelsSelect { const labelUrl = $dropdown.attr('data-labels'); axios .get(labelUrl) - .then(res => { + .then((res) => { let { data } = res; if ($dropdown.hasClass('js-extra-options')) { const extraData = []; @@ -210,9 +210,7 @@ export default class LabelsSelect { } } else { if (this.id(label)) { - const dropdownValue = this.id(label) - .toString() - .replace(/'/g, "\\'"); + const dropdownValue = this.id(label).toString().replace(/'/g, "\\'"); if ( $form.find( @@ -346,10 +344,7 @@ export default class LabelsSelect { const isMRIndex = page === 'projects:merge_requests:index'; if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) { - $dropdown - .parent() - .find('.dropdown-clear-active') - .removeClass('is-active'); + $dropdown.parent().find('.dropdown-clear-active').removeClass('is-active'); } if ($dropdown.hasClass('js-issuable-form-dropdown')) { @@ -395,7 +390,7 @@ export default class LabelsSelect { ); } else { let { labels } = boardsStore.detail.issue; - labels = labels.filter(selectedLabel => selectedLabel.id !== label.id); + labels = labels.filter((selectedLabel) => selectedLabel.id !== label.id); boardsStore.detail.issue.labels = labels; } @@ -406,12 +401,12 @@ export default class LabelsSelect { .update($dropdown.attr('data-issue-update')) .then(() => { if (isScopedLabel(label)) { - const prevIds = oldLabels.map(label => label.id); - const newIds = boardsStore.detail.issue.labels.map(label => label.id); - const differentIds = prevIds.filter(x => !newIds.includes(x)); + const prevIds = oldLabels.map((label) => label.id); + const newIds = boardsStore.detail.issue.labels.map((label) => label.id); + const differentIds = prevIds.filter((x) => !newIds.includes(x)); $dropdown.data('marked', newIds); $dropdownMenu - .find(differentIds.map(id => `[data-label-id="${id}"]`).join(',')) + .find(differentIds.map((id) => `[data-label-id="${id}"]`).join(',')) .removeClass('is-active'); } }) @@ -559,7 +554,7 @@ export default class LabelsSelect { // eslint-disable-next-line class-methods-use-this setOriginalDropdownData($container, $dropdown) { const labels = []; - $container.find('[name="label_name[]"]').map(function() { + $container.find('[name="label_name[]"]').map(function () { return labels.push(this.value); }); $dropdown.data('marked', labels); diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js index aa7fe087678..b7cb6aa0a21 100644 --- a/app/assets/javascripts/lazy_loader.js +++ b/app/assets/javascripts/lazy_loader.js @@ -28,10 +28,10 @@ export default class LazyLoader { const lazyImages = [].slice.call(document.querySelectorAll('.lazy')); if (LazyLoader.supportsNativeLazyLoading()) { - lazyImages.forEach(img => LazyLoader.loadImage(img)); + lazyImages.forEach((img) => LazyLoader.loadImage(img)); } else if (LazyLoader.supportsIntersectionObserver()) { if (this.intersectionObserver) { - lazyImages.forEach(img => this.intersectionObserver.observe(img)); + lazyImages.forEach((img) => this.intersectionObserver.observe(img)); } } else if (lazyImages.length) { this.lazyImages = lazyImages; @@ -98,8 +98,8 @@ export default class LazyLoader { }); }; - onIntersection = entries => { - entries.forEach(entry => { + onIntersection = (entries) => { + entries.forEach((entry) => { // We are using `intersectionRatio > 0` over `isIntersecting`, as some browsers did not ship the latter // See: https://gitlab.com/gitlab-org/gitlab-foss/issues/54407 if (entry.intersectionRatio > 0) { @@ -126,7 +126,7 @@ export default class LazyLoader { const visHeight = scrollTop + window.innerHeight + SCROLL_THRESHOLD; // Loading Images which are in the current viewport or close to them - this.lazyImages = this.lazyImages.filter(selectedImage => { + this.lazyImages = this.lazyImages.filter((selectedImage) => { if (selectedImage.getAttribute('data-src')) { const imgBoundRect = selectedImage.getBoundingClientRect(); const imgTop = scrollTop + imgBoundRect.top; diff --git a/app/assets/javascripts/lib/chrome_84_icon_fix.js b/app/assets/javascripts/lib/chrome_84_icon_fix.js index 60497186c19..20fe9590ce3 100644 --- a/app/assets/javascripts/lib/chrome_84_icon_fix.js +++ b/app/assets/javascripts/lib/chrome_84_icon_fix.js @@ -30,7 +30,7 @@ document.addEventListener('DOMContentLoaded', async () => { const fixSVGs = () => { requestIdleCallback(() => { - document.querySelectorAll(`use:not([${SKIP_ATTRIBUTE}])`).forEach(use => { + document.querySelectorAll(`use:not([${SKIP_ATTRIBUTE}])`).forEach((use) => { const href = use?.getAttribute('href') ?? use?.getAttribute('xlink:href') ?? ''; if (href.includes(window.gon.sprite_icons)) { @@ -60,7 +60,7 @@ document.addEventListener('DOMContentLoaded', async () => { div.classList.add('hidden'); const result = await fetch(url); div.innerHTML = await result.text(); - div.querySelectorAll('[id]').forEach(node => { + div.querySelectorAll('[id]').forEach((node) => { node.setAttribute('id', `${prefix}-${node.getAttribute('id')}`); }); document.body.append(div); diff --git a/app/assets/javascripts/lib/dompurify.js b/app/assets/javascripts/lib/dompurify.js index d9ea57fbbce..76624c81ed5 100644 --- a/app/assets/javascripts/lib/dompurify.js +++ b/app/assets/javascripts/lib/dompurify.js @@ -11,9 +11,9 @@ const defaultConfig = { const getAllowedIconUrls = (gon = window.gon) => [gon.sprite_file_icons, gon.sprite_icons].filter(Boolean); -const isUrlAllowed = url => getAllowedIconUrls().some(allowedUrl => url.startsWith(allowedUrl)); +const isUrlAllowed = (url) => getAllowedIconUrls().some((allowedUrl) => url.startsWith(allowedUrl)); -const isHrefSafe = url => +const isHrefSafe = (url) => isUrlAllowed(url) || isUrlAllowed(relativePathToAbsolute(url, getBaseURL())); const removeUnsafeHref = (node, attr) => { @@ -36,7 +36,7 @@ const removeUnsafeHref = (node, attr) => { * * @param {Object} node - Node to sanitize */ -const sanitizeSvgIcon = node => { +const sanitizeSvgIcon = (node) => { removeUnsafeHref(node, 'href'); // Note: `xlink:href` is deprecated, but still in use @@ -44,7 +44,7 @@ const sanitizeSvgIcon = node => { removeUnsafeHref(node, 'xlink:href'); }; -addHook('afterSanitizeAttributes', node => { +addHook('afterSanitizeAttributes', (node) => { if (node.tagName.toLowerCase() === 'use') { sanitizeSvgIcon(node); } diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js index e0d9a903e0a..5c4bb5ea01f 100644 --- a/app/assets/javascripts/lib/graphql.js +++ b/app/assets/javascripts/lib/graphql.js @@ -36,13 +36,13 @@ export default (resolvers = {}, config = {}) => { }; const uploadsLink = ApolloLink.split( - operation => operation.getContext().hasUpload || operation.getContext().isSingleRequest, + (operation) => operation.getContext().hasUpload || operation.getContext().isSingleRequest, createUploadLink(httpOptions), new BatchHttpLink(httpOptions), ); const performanceBarLink = new ApolloLink((operation, forward) => { - return forward(operation).map(response => { + return forward(operation).map((response) => { const httpResponse = operation.getContext().response; if (PerformanceBarService.interceptor) { diff --git a/app/assets/javascripts/lib/utils/ajax_cache.js b/app/assets/javascripts/lib/utils/ajax_cache.js index 2d976dbdbbe..935bd0f16e9 100644 --- a/app/assets/javascripts/lib/utils/ajax_cache.js +++ b/app/assets/javascripts/lib/utils/ajax_cache.js @@ -25,7 +25,7 @@ class AjaxCache extends Cache { this.internalStorage[endpoint] = data; delete this.pendingRequests[endpoint]; }) - .catch(e => { + .catch((e) => { const error = new Error(`${endpoint}: ${e.message}`); error.textStatus = e.message; diff --git a/app/assets/javascripts/lib/utils/apollo_startup_js_link.js b/app/assets/javascripts/lib/utils/apollo_startup_js_link.js index 5c120dd532f..014823f3831 100644 --- a/app/assets/javascripts/lib/utils/apollo_startup_js_link.js +++ b/app/assets/javascripts/lib/utils/apollo_startup_js_link.js @@ -7,7 +7,7 @@ import { isEqual, pickBy } from 'lodash'; * @param obj * @returns {Dictionary<unknown>} */ -const pickDefinedValues = obj => pickBy(obj, x => x !== undefined); +const pickDefinedValues = (obj) => pickBy(obj, (x) => x !== undefined); /** * Compares two set of variables, order independent @@ -28,9 +28,9 @@ export class StartupJSLink extends ApolloLink { // Extract operationNames from the queries and ensure that we can // match operationName => element from result array parseStartupCalls(calls) { - calls.forEach(call => { + calls.forEach((call) => { const { query, variables, fetchCall } = call; - const operationName = parse(query)?.definitions?.find(x => x.kind === 'OperationDefinition') + const operationName = parse(query)?.definitions?.find((x) => x.kind === 'OperationDefinition') ?.name?.value; if (operationName) { @@ -71,9 +71,9 @@ export class StartupJSLink extends ApolloLink { return forward(operation); } - return new Observable(observer => { + return new Observable((observer) => { fetchCall - .then(response => { + .then((response) => { // Handle HTTP errors if (!response.ok) { throw new Error('fetchCall failed'); @@ -81,7 +81,7 @@ export class StartupJSLink extends ApolloLink { operation.setContext({ response }); return response.json(); }) - .then(result => { + .then((result) => { if (result && (result.errors || !result.data)) { throw new Error('Received GraphQL error'); } @@ -92,10 +92,10 @@ export class StartupJSLink extends ApolloLink { }) .catch(() => { forward(operation).subscribe({ - next: result => { + next: (result) => { observer.next(result); }, - error: error => { + error: (error) => { observer.error(error); }, complete: observer.complete.bind(observer), diff --git a/app/assets/javascripts/lib/utils/autosave.js b/app/assets/javascripts/lib/utils/autosave.js index 56df2532528..dac1da743a2 100644 --- a/app/assets/javascripts/lib/utils/autosave.js +++ b/app/assets/javascripts/lib/utils/autosave.js @@ -1,6 +1,6 @@ import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; -export const clearDraft = autosaveKey => { +export const clearDraft = (autosaveKey) => { try { window.localStorage.removeItem(`autosave/${autosaveKey}`); } catch (e) { @@ -9,7 +9,7 @@ export const clearDraft = autosaveKey => { } }; -export const getDraft = autosaveKey => { +export const getDraft = (autosaveKey) => { try { return window.localStorage.getItem(`autosave/${autosaveKey}`); } catch (e) { diff --git a/app/assets/javascripts/lib/utils/axios_startup_calls.js b/app/assets/javascripts/lib/utils/axios_startup_calls.js index 7bb1da5aed5..f9d58ff9b1d 100644 --- a/app/assets/javascripts/lib/utils/axios_startup_calls.js +++ b/app/assets/javascripts/lib/utils/axios_startup_calls.js @@ -3,9 +3,9 @@ import { mergeUrlParams } from './url_utility'; // We should probably not couple this utility to `gon.gitlab_url` // Also, this would replace occurrences that aren't at the beginning of the string -const removeGitLabUrl = url => url.replace(gon.gitlab_url, ''); +const removeGitLabUrl = (url) => url.replace(gon.gitlab_url, ''); -const getFullUrl = req => { +const getFullUrl = (req) => { const url = removeGitLabUrl(req.url); return mergeUrlParams(req.params || {}, url, { sort: true }); }; @@ -36,7 +36,7 @@ const handleStartupCall = async ({ fetchCall }, req) => { }); }; -const setupAxiosStartupCalls = axios => { +const setupAxiosStartupCalls = (axios) => { const { startup_calls: startupCalls } = window.gl || {}; if (!startupCalls || isEmpty(startupCalls)) { @@ -45,7 +45,7 @@ const setupAxiosStartupCalls = axios => { const remainingCalls = new Map(Object.entries(startupCalls)); - const interceptor = axios.interceptors.request.use(async req => { + const interceptor = axios.interceptors.request.use(async (req) => { const fullUrl = getFullUrl(req); const startupCall = remainingCalls.get(fullUrl); diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js index 9d517f45caa..cb479e243b2 100644 --- a/app/assets/javascripts/lib/utils/axios_utils.js +++ b/app/assets/javascripts/lib/utils/axios_utils.js @@ -9,7 +9,7 @@ axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; // Maintain a global counter for active requests // see: spec/support/wait_for_requests.rb -axios.interceptors.request.use(config => { +axios.interceptors.request.use((config) => { window.pendingRequests = window.pendingRequests || 0; window.pendingRequests += 1; return config; @@ -19,11 +19,11 @@ setupAxiosStartupCalls(axios); // Remove the global counter axios.interceptors.response.use( - response => { + (response) => { window.pendingRequests -= 1; return response; }, - err => { + (err) => { window.pendingRequests -= 1; return Promise.reject(err); }, @@ -37,8 +37,8 @@ window.addEventListener('beforeunload', () => { // Ignore AJAX errors caused by requests // being cancelled due to browser navigation axios.interceptors.response.use( - response => response, - err => suppressAjaxErrorsDuringNavigation(err, isUserNavigating), + (response) => response, + (err) => suppressAjaxErrorsDuringNavigation(err, isUserNavigating), ); export default axios; diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js index 28a7ebfdc69..286fc2568b2 100644 --- a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js +++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js @@ -68,7 +68,7 @@ export default class LinkedTabs { // since this is a custom event we need jQuery :( $(document) .off('shown.bs.tab', tabSelector) - .on('shown.bs.tab', tabSelector, e => this.tabShown(e)); + .on('shown.bs.tab', tabSelector, (e) => this.tabShown(e)); this.activateTab(this.action); } diff --git a/app/assets/javascripts/lib/utils/chart_utils.js b/app/assets/javascripts/lib/utils/chart_utils.js index 4a1e6c5d68c..7da3bab0a4b 100644 --- a/app/assets/javascripts/lib/utils/chart_utils.js +++ b/app/assets/javascripts/lib/utils/chart_utils.js @@ -34,7 +34,7 @@ const commonChartOptions = () => ({ legend: false, }); -export const barChartOptions = shouldAdjustFontSize => ({ +export const barChartOptions = (shouldAdjustFontSize) => ({ ...commonChartOptions(), scales: { ...yAxesConfig(shouldAdjustFontSize), @@ -89,7 +89,7 @@ export const lineChartOptions = ({ width, numberOfPoints, shouldAdjustFontSize } * @param {Array} data * @returns {[*, *]} */ -export const firstAndLastY = data => { +export const firstAndLastY = (data) => { const [firstEntry] = data; const [lastEntry] = data.slice(-1); diff --git a/app/assets/javascripts/lib/utils/color_utils.js b/app/assets/javascripts/lib/utils/color_utils.js index 07fb2915ca7..a1f56b15631 100644 --- a/app/assets/javascripts/lib/utils/color_utils.js +++ b/app/assets/javascripts/lib/utils/color_utils.js @@ -4,7 +4,7 @@ * @param hex string * @returns array|null */ -export const hexToRgb = hex => { +export const hexToRgb = (hex) => { // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; const fullHex = hex.replace(shorthandRegex, (_m, r, g, b) => r + r + g + g + b + b); @@ -15,7 +15,7 @@ export const hexToRgb = hex => { : null; }; -export const textColorForBackground = backgroundColor => { +export const textColorForBackground = (backgroundColor) => { const [r, g, b] = hexToRgb(backgroundColor); if (r + g + b > 500) { diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index f88a0433535..128ef5b335e 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -4,9 +4,8 @@ import { GlBreakpointInstance as breakpointInstance } from '@gitlab/ui/dist/utils'; import $ from 'jquery'; -import { isFunction } from 'lodash'; +import { isFunction, defer } from 'lodash'; import Cookies from 'js-cookie'; -import axios from './axios_utils'; import { getLocationHash } from './url_utility'; import { convertToCamelCase, convertToSnakeCase } from './text_utility'; import { isObject } from './type_utility'; @@ -54,7 +53,7 @@ export const getCspNonceValue = () => { return metaTag && metaTag.content; }; -export const rstrip = val => { +export const rstrip = (val) => { if (val) { return val.replace(/\s+$/, ''); } @@ -68,7 +67,7 @@ export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventNa closestSubmit.disable(); } // eslint-disable-next-line func-names - return field.on(eventName, function() { + return field.on(eventName, function () { if (rstrip($(this).val()) === '') { return closestSubmit.disable(); } @@ -149,13 +148,13 @@ export const isInViewport = (el, offset = {}) => { ); }; -export const parseUrl = url => { +export const parseUrl = (url) => { const parser = document.createElement('a'); parser.href = url; return parser; }; -export const parseUrlPathname = url => { +export const parseUrlPathname = (url) => { const parsedUrl = parseUrl(url); // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11 // We have to make sure we always have an absolute path. @@ -166,8 +165,8 @@ const splitPath = (path = '') => path.replace(/^\?/, '').split('&'); export const urlParamsToArray = (path = '') => splitPath(path) - .filter(param => param.length > 0) - .map(param => { + .filter((param) => param.length > 0) + .map((param) => { const split = param.split('='); return [decodeURI(split[0]), split[1]].join('='); }); @@ -209,13 +208,13 @@ export const urlParamsToObject = (path = '') => return data; }, {}); -export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; +export const isMetaKey = (e) => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; // Identify following special clicks // 1) Cmd + Click on Mac (e.metaKey) // 2) Ctrl + Click on PC (e.ctrlKey) // 3) Middle-click or Mouse Wheel Click (e.which is 2) -export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2; +export const isMetaClick = (e) => e.metaKey || e.ctrlKey || e.which === 2; export const contentTop = () => { const isDesktop = breakpointInstance.isDesktop(); @@ -261,23 +260,26 @@ export const contentTop = () => { }; export const scrollToElement = (element, options = {}) => { - let $el = element; - if (!(element instanceof $)) { - $el = $(element); + let el = element; + if (element instanceof $) { + // eslint-disable-next-line prefer-destructuring + el = element[0]; + } else if (typeof el === 'string') { + el = document.querySelector(element); } - const { top } = $el.offset(); - const { offset = 0 } = options; - // eslint-disable-next-line no-jquery/no-animate - return $('body, html').animate( - { - scrollTop: top - contentTop() + offset, - }, - 200, - ); + if (el && el.getBoundingClientRect) { + // In the previous implementation, jQuery naturally deferred this scrolling. + // Unfortunately, we're quite coupled to this implementation detail now. + defer(() => { + const { duration = 200, offset = 0 } = options; + const y = el.getBoundingClientRect().top + window.pageYOffset + offset - contentTop(); + window.scrollTo({ top: y, behavior: duration ? 'smooth' : 'auto' }); + }); + } }; -export const scrollToElementWithContext = element => { +export const scrollToElementWithContext = (element) => { const offsetMultiplier = -0.1; return scrollToElement(element, { offset: window.innerHeight * offsetMultiplier }); }; @@ -287,7 +289,7 @@ export const scrollToElementWithContext = element => { * each browser screen repaint. * @param {Function} fn */ -export const debounceByAnimationFrame = fn => { +export const debounceByAnimationFrame = (fn) => { let requestId; return function debounced(...args) { @@ -334,7 +336,7 @@ const handleSelectedRange = (range, restrictToNode) => { return range.cloneContents(); }; -export const getSelectedFragment = restrictToNode => { +export const getSelectedFragment = (restrictToNode) => { const selection = window.getSelection(); if (selection.rangeCount === 0) return null; // Most usages of the selection only want text from a part of the page (e.g. discussion) @@ -390,10 +392,10 @@ export const insertText = (target, text) => { this will take in the headers from an API response and normalize them this way we don't run into production issues when nginx gives us lowercased header keys */ -export const normalizeHeaders = headers => { +export const normalizeHeaders = (headers) => { const upperCaseHeaders = {}; - Object.keys(headers || {}).forEach(e => { + Object.keys(headers || {}).forEach((e) => { upperCaseHeaders[e.toUpperCase()] = headers[e]; }); @@ -406,7 +408,7 @@ export const normalizeHeaders = headers => { * @param {Object} paginationInformation * @returns {Object} */ -export const parseIntPagination = paginationInformation => ({ +export const parseIntPagination = (paginationInformation) => ({ perPage: parseInt(paginationInformation['X-PER-PAGE'], 10), page: parseInt(paginationInformation['X-PAGE'], 10), total: parseInt(paginationInformation['X-TOTAL'], 10), @@ -445,10 +447,10 @@ export const parseQueryStringIntoObject = (query = '') => { */ export const objectToQueryString = (params = {}) => Object.keys(params) - .map(param => `${param}=${params[param]}`) + .map((param) => `${param}=${params[param]}`) .join('&'); -export const buildUrlWithCurrentLocation = param => { +export const buildUrlWithCurrentLocation = (param) => { if (param) return `${window.location.pathname}${param}`; return window.location.pathname; @@ -460,7 +462,7 @@ export const buildUrlWithCurrentLocation = param => { * * @param {String} param */ -export const historyPushState = newUrl => { +export const historyPushState = (newUrl) => { window.history.pushState({}, document.title, newUrl); }; @@ -470,7 +472,7 @@ export const historyPushState = newUrl => { * * @param {String} param */ -export const historyReplaceState = newUrl => { +export const historyReplaceState = (newUrl) => { window.history.replaceState({}, document.title, newUrl); }; @@ -482,7 +484,7 @@ export const historyReplaceState = newUrl => { * @param {String} value * @returns {Boolean} */ -export const parseBoolean = value => (value && value.toString()) === 'true'; +export const parseBoolean = (value) => (value && value.toString()) === 'true'; export const BACKOFF_TIMEOUT = 'BACKOFF_TIMEOUT'; @@ -529,7 +531,7 @@ export const backOff = (fn, timeout = 60000) => { let timeElapsed = 0; return new Promise((resolve, reject) => { - const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg)); + const stop = (arg) => (arg instanceof Error ? reject(arg) : resolve(arg)); const next = () => { if (timeElapsed < timeout) { @@ -545,92 +547,6 @@ export const backOff = (fn, timeout = 60000) => { }); }; -export const createOverlayIcon = (iconPath, overlayPath) => { - const faviconImage = document.createElement('img'); - - return new Promise(resolve => { - faviconImage.onload = () => { - const size = 32; - - const canvas = document.createElement('canvas'); - canvas.width = size; - canvas.height = size; - - const context = canvas.getContext('2d'); - context.clearRect(0, 0, size, size); - context.drawImage( - faviconImage, - 0, - 0, - faviconImage.width, - faviconImage.height, - 0, - 0, - size, - size, - ); - - const overlayImage = document.createElement('img'); - overlayImage.onload = () => { - context.drawImage( - overlayImage, - 0, - 0, - overlayImage.width, - overlayImage.height, - 0, - 0, - size, - size, - ); - - const faviconWithOverlayUrl = canvas.toDataURL(); - - resolve(faviconWithOverlayUrl); - }; - overlayImage.src = overlayPath; - }; - faviconImage.src = iconPath; - }); -}; - -export const setFaviconOverlay = overlayPath => { - const faviconEl = document.getElementById('favicon'); - - if (!faviconEl) { - return null; - } - - const iconPath = faviconEl.getAttribute('data-original-href'); - - return createOverlayIcon(iconPath, overlayPath).then(faviconWithOverlayUrl => - faviconEl.setAttribute('href', faviconWithOverlayUrl), - ); -}; - -export const resetFavicon = () => { - const faviconEl = document.getElementById('favicon'); - - if (faviconEl) { - const originalFavicon = faviconEl.getAttribute('data-original-href'); - faviconEl.setAttribute('href', originalFavicon); - } -}; - -export const setCiStatusFavicon = pageUrl => - axios - .get(pageUrl) - .then(({ data }) => { - if (data && data.favicon) { - return setFaviconOverlay(data.favicon); - } - return resetFavicon(); - }) - .catch(error => { - resetFavicon(); - throw error; - }); - export const spriteIcon = (icon, className = '') => { const classAttribute = className.length > 0 ? `class="${className}"` : ''; @@ -728,7 +644,7 @@ export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => export const convertObjectPropsToSnakeCase = (obj = {}, options = {}) => convertObjectProps(convertToSnakeCase, obj, options); -export const imagePath = imgUrl => +export const imagePath = (imgUrl) => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`; export const addSelectOnFocusBehaviour = (selector = '.js-select-on-focus') => { @@ -737,7 +653,7 @@ export const addSelectOnFocusBehaviour = (selector = '.js-select-on-focus') => { $(selector).on('focusin', function selectOnFocusCallback() { $(this) .select() - .one('mouseup', e => { + .one('mouseup', (e) => { e.preventDefault(); }); }); @@ -833,7 +749,7 @@ export const searchBy = (query = '', searchSpace = {}) => { const normalizedQuery = query.toLowerCase(); const matches = targetKeys - .filter(item => { + .filter((item) => { const searchItem = `${searchSpace[item]}`.toLowerCase(); return ( @@ -867,9 +783,9 @@ export const isScopedLabel = ({ title = '' }) => title.indexOf('::') !== -1; // Methods to set and get Cookie export const setCookie = (name, value) => Cookies.set(name, value, { expires: 365 }); -export const getCookie = name => Cookies.get(name); +export const getCookie = (name) => Cookies.get(name); -export const removeCookie = name => Cookies.remove(name); +export const removeCookie = (name) => Cookies.remove(name); /** * Returns the status of a feature flag. @@ -884,4 +800,4 @@ export const removeCookie = name => Cookies.remove(name); * @param {String} flag Feature flag * @returns {Boolean} on/off */ -export const isFeatureFlagEnabled = flag => window.gon.features?.[flag]; +export const isFeatureFlagEnabled = (flag) => window.gon.features?.[flag]; diff --git a/app/assets/javascripts/lib/utils/css_utils.js b/app/assets/javascripts/lib/utils/css_utils.js index 02f092e73e1..76ac442a470 100644 --- a/app/assets/javascripts/lib/utils/css_utils.js +++ b/app/assets/javascripts/lib/utils/css_utils.js @@ -1,5 +1,5 @@ export function loadCSSFile(path) { - return new Promise(resolve => { + return new Promise((resolve) => { if (!path) resolve(); if (document.querySelector(`link[href="${path}"]`)) { diff --git a/app/assets/javascripts/lib/utils/datetime_range.js b/app/assets/javascripts/lib/utils/datetime_range.js index 8efbcb89607..391b685f740 100644 --- a/app/assets/javascripts/lib/utils/datetime_range.js +++ b/app/assets/javascripts/lib/utils/datetime_range.js @@ -7,7 +7,7 @@ const MINIMUM_DATE = new Date(0); const DEFAULT_DIRECTION = 'before'; -const durationToMillis = duration => { +const durationToMillis = (duration) => { if (Object.entries(duration).length === 1 && Number.isFinite(duration.seconds)) { return secondsToMilliseconds(duration.seconds); } @@ -19,9 +19,9 @@ const dateMinusDuration = (date, duration) => new Date(date.getTime() - duration const datePlusDuration = (date, duration) => new Date(date.getTime() + durationToMillis(duration)); -const isValidDuration = duration => Boolean(duration && Number.isFinite(duration.seconds)); +const isValidDuration = (duration) => Boolean(duration && Number.isFinite(duration.seconds)); -const isValidDateString = dateString => { +const isValidDateString = (dateString) => { if (typeof dateString !== 'string' || !dateString.trim()) { return false; } @@ -225,7 +225,7 @@ export function getRangeType(range) { * * @returns {FixedRange} An object with a start and end in ISO8601 format. */ -export const convertToFixedRange = dateTimeRange => +export const convertToFixedRange = (dateTimeRange) => handlers[getRangeType(dateTimeRange)](dateTimeRange); /** @@ -242,7 +242,7 @@ export const convertToFixedRange = dateTimeRange => * @param {Object} timeRange - A time range object * @returns Copy of time range */ -const pruneTimeRange = timeRange => { +const pruneTimeRange = (timeRange) => { const res = pick(timeRange, ['start', 'end', 'anchor', 'duration', 'direction']); if (res.direction === DEFAULT_DIRECTION) { return omit(res, 'direction'); @@ -272,7 +272,7 @@ export const isEqualTimeRanges = (timeRange, other) => { * @param {Array} timeRanges - Array of time tanges (haystack) */ export const findTimeRange = (timeRange, timeRanges) => - timeRanges.find(element => isEqualTimeRanges(element, timeRange)); + timeRanges.find((element) => isEqualTimeRanges(element, timeRange)); // Time Ranges as URL Parameters Utils @@ -289,11 +289,11 @@ export const timeRangeParamNames = ['start', 'end', 'anchor', 'duration_seconds' * @param {Object} A time range * @returns key-value pairs object that can be used as parameters in a URL. */ -export const timeRangeToParams = timeRange => { +export const timeRangeToParams = (timeRange) => { let params = pruneTimeRange(timeRange); if (timeRange.duration) { const durationParms = {}; - Object.keys(timeRange.duration).forEach(key => { + Object.keys(timeRange.duration).forEach((key) => { durationParms[`duration_${key}`] = timeRange.duration[key].toString(); }); params = { ...durationParms, ...params }; @@ -309,7 +309,7 @@ export const timeRangeToParams = timeRange => { * * @param {params} params - key-value pairs object. */ -export const timeRangeFromParams = params => { +export const timeRangeFromParams = (params) => { const timeRangeParams = pick(params, timeRangeParamNames); let range = Object.entries(timeRangeParams).reduce((acc, [key, val]) => { // unflatten duration diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 46b0f0cbc70..15f7c0c874e 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -4,6 +4,8 @@ import * as timeago from 'timeago.js'; import dateFormat from 'dateformat'; import { languageCode, s__, __, n__ } from '../../locale'; +const MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; + window.timeago = timeago; /** @@ -12,7 +14,7 @@ window.timeago = timeago; * * @param {Date} date */ -export const newDate = date => (date instanceof Date ? new Date(date.getTime()) : new Date()); +export const newDate = (date) => (date instanceof Date ? new Date(date.getTime()) : new Date()); /** * Returns i18n month names array. @@ -21,7 +23,7 @@ export const newDate = date => (date instanceof Date ? new Date(date.getTime()) * * @param {Boolean} abbreviated */ -export const getMonthNames = abbreviated => { +export const getMonthNames = (abbreviated) => { if (abbreviated) { return [ s__('Jan'), @@ -74,7 +76,7 @@ export const getWeekdayNames = () => [ * @param {date} date * @returns {String} */ -export const getDayName = date => +export const getDayName = (date) => [ __('Sunday'), __('Monday'), @@ -242,7 +244,7 @@ export const getDayDifference = (a, b) => { * @param {Number} seconds * @return {String} */ -export const timeIntervalInWords = intervalInSeconds => { +export const timeIntervalInWords = (intervalInSeconds) => { const secondsInteger = parseInt(intervalInSeconds, 10); const minutes = Math.floor(secondsInteger / 60); const seconds = secondsInteger - minutes * 60; @@ -316,7 +318,7 @@ export const monthInWords = (date, abbreviated = false) => { * * @param {Date} date */ -export const totalDaysInMonth = date => { +export const totalDaysInMonth = (date) => { if (!date) { return 0; } @@ -329,7 +331,7 @@ export const totalDaysInMonth = date => { * * @param {Array} quarter */ -export const totalDaysInQuarter = quarter => +export const totalDaysInQuarter = (quarter) => quarter.reduce((acc, month) => acc + totalDaysInMonth(month), 0); /** @@ -338,7 +340,7 @@ export const totalDaysInQuarter = quarter => * * @param {Date} date */ -export const getSundays = date => { +export const getSundays = (date) => { if (!date) { return []; } @@ -449,7 +451,7 @@ window.gl.utils = { * @param milliseconds * @returns {string} */ -export const formatTime = milliseconds => { +export const formatTime = (milliseconds) => { const remainingSeconds = Math.floor(milliseconds / 1000) % 60; const remainingMinutes = Math.floor(milliseconds / 1000 / 60) % 60; const remainingHours = Math.floor(milliseconds / 1000 / 60 / 60); @@ -468,7 +470,7 @@ export const formatTime = milliseconds => { * @param {String} dateString Date in yyyy-mm-dd format * @return {Date} UTC format */ -export const parsePikadayDate = dateString => { +export const parsePikadayDate = (dateString) => { const parts = dateString.split('-'); const year = parseInt(parts[0], 10); const month = parseInt(parts[1] - 1, 10); @@ -482,7 +484,7 @@ export const parsePikadayDate = dateString => { * @param {Date} date UTC format * @return {String} Date formatted in yyyy-mm-dd */ -export const pikadayToString = date => { +export const pikadayToString = (date) => { const day = pad(date.getDate()); const month = pad(date.getMonth() + 1); const year = date.getFullYear(); @@ -523,7 +525,7 @@ export const parseSeconds = ( let unorderedMinutes = Math.abs(seconds / SECONDS_PER_MINUTE); - return mapValues(timePeriodConstraints, minutesPerPeriod => { + return mapValues(timePeriodConstraints, (minutesPerPeriod) => { if (minutesPerPeriod === 0) { return 0; } @@ -567,7 +569,7 @@ export const stringifyTime = (timeObject, fullNameFormat = false) => { * @param endDate date string that the time difference is calculated for * @return {Number} number of milliseconds remaining until the given date */ -export const calculateRemainingMilliseconds = endDate => { +export const calculateRemainingMilliseconds = (endDate) => { const remainingMilliseconds = new Date(endDate).getTime() - Date.now(); return Math.max(remainingMilliseconds, 0); }; @@ -598,7 +600,7 @@ export const getDateInFuture = (date, daysInFuture) => * @param {Date} date * @returns boolean */ -export const isValidDate = date => date instanceof Date && !Number.isNaN(date.getTime()); +export const isValidDate = (date) => date instanceof Date && !Number.isNaN(date.getTime()); /* * Appending T00:00:00 makes JS assume local time and prevents it from shifting the date @@ -606,7 +608,7 @@ export const isValidDate = date => date instanceof Date && !Number.isNaN(date.ge * be consistent with the "edit issue -> due date" UI. */ -export const newDateAsLocaleTime = date => { +export const newDateAsLocaleTime = (date) => { const suffix = 'T00:00:00'; return new Date(`${date}${suffix}`); }; @@ -620,7 +622,7 @@ export const endOfDayTime = 'T23:59:59Z'; * @param {Function} formatter * @return {Any[]} an array of formatted dates between 2 given dates (including start&end date) */ -export const getDatesInRange = (d1, d2, formatter = x => x) => { +export const getDatesInRange = (d1, d2, formatter = (x) => x) => { if (!(d1 instanceof Date) || !(d2 instanceof Date)) { return []; } @@ -643,7 +645,7 @@ export const getDatesInRange = (d1, d2, formatter = x => x) => { * @param {Number} seconds * @return {Number} number of milliseconds */ -export const secondsToMilliseconds = seconds => seconds * 1000; +export const secondsToMilliseconds = (seconds) => seconds * 1000; /** * Converts the supplied number of seconds to days. @@ -651,7 +653,7 @@ export const secondsToMilliseconds = seconds => seconds * 1000; * @param {Number} seconds * @return {Number} number of days */ -export const secondsToDays = seconds => Math.round(seconds / 86400); +export const secondsToDays = (seconds) => Math.round(seconds / 86400); /** * Converts a numeric utc offset in seconds to +/- hours @@ -662,7 +664,7 @@ export const secondsToDays = seconds => Math.round(seconds / 86400); * * @return {String} the + or - offset in hours */ -export const secondsToHours = offset => { +export const secondsToHours = (offset) => { const parsed = parseInt(offset, 10); if (Number.isNaN(parsed) || parsed === 0) { return `0`; @@ -682,12 +684,40 @@ export const nDaysAfter = (date, numberOfDays) => new Date(newDate(date)).setDate(date.getDate() + numberOfDays); /** + * Returns the date n days before the date provided + * + * @param {Date} date the initial date + * @param {Number} numberOfDays number of days before + * @return {Date} the date preceding the date provided + */ +export const nDaysBefore = (date, numberOfDays) => nDaysAfter(date, -numberOfDays); + +/** + * Returns the date n months after the date provided + * + * @param {Date} date the initial date + * @param {Number} numberOfMonths number of months after + * @return {Date} the date following the date provided + */ +export const nMonthsAfter = (date, numberOfMonths) => + new Date(newDate(date)).setMonth(date.getMonth() + numberOfMonths); + +/** + * Returns the date n months before the date provided + * + * @param {Date} date the initial date + * @param {Number} numberOfMonths number of months before + * @return {Date} the date preceding the date provided + */ +export const nMonthsBefore = (date, numberOfMonths) => nMonthsAfter(date, -numberOfMonths); + +/** * Returns the date after the date provided * * @param {Date} date the initial date * @return {Date} the date following the date provided */ -export const dayAfter = date => new Date(newDate(date).setDate(date.getDate() + 1)); +export const dayAfter = (date) => new Date(newDate(date).setDate(date.getDate() + 1)); /** * Mimics the behaviour of the rails distance_of_time_in_words function @@ -795,7 +825,7 @@ export const differenceInMilliseconds = (startDate, endDate = Date.now()) => { * * @return {Date} the date at the first day of the month */ -export const dateAtFirstDayOfMonth = date => new Date(newDate(date).setDate(1)); +export const dateAtFirstDayOfMonth = (date) => new Date(newDate(date).setDate(1)); /** * A utility function which checks if two dates match. @@ -806,3 +836,62 @@ export const dateAtFirstDayOfMonth = date => new Date(newDate(date).setDate(1)); * @return {Boolean} true if the dates match */ export const datesMatch = (date1, date2) => differenceInMilliseconds(date1, date2) === 0; + +/** + * A utility function which computes a formatted 24 hour + * time string from a positive int in the range 0 - 24. + * + * @param {Int} time a positive Int between 0 and 24 + * + * @returns {String} formatted 24 hour time String + */ +export const format24HourTimeStringFromInt = (time) => { + if (!Number.isInteger(time) || time < 0 || time > 24) { + return ''; + } + + const formatted24HourString = time > 9 ? `${time}:00` : `0${time}:00`; + return formatted24HourString; +}; + +/** + * A utility function which checks if two date ranges overlap. + * + * @param {Object} givenPeriodLeft - the first period to compare. + * @param {Object} givenPeriodRight - the second period to compare. + * @returns {Object} { overlap: number of days the overlap is present, overlapStartDate: the start date of the overlap in time format, overlapEndDate: the end date of the overlap in time format } + * @throws {Error} Uncaught Error: Invalid period + * + * @example + * getOverlappingDaysInPeriods( + * { start: new Date(2021, 0, 11), end: new Date(2021, 0, 13) }, + * { start: new Date(2021, 0, 11), end: new Date(2021, 0, 14) } + * ) => { daysOverlap: 2, overlapStartDate: 1610323200000, overlapEndDate: 1610496000000 } + * + */ +export const getOverlappingDaysInPeriods = (givenPeriodLeft = {}, givenPeriodRight = {}) => { + const leftStartTime = new Date(givenPeriodLeft.start).getTime(); + const leftEndTime = new Date(givenPeriodLeft.end).getTime(); + const rightStartTime = new Date(givenPeriodRight.start).getTime(); + const rightEndTime = new Date(givenPeriodRight.end).getTime(); + + if (!(leftStartTime <= leftEndTime && rightStartTime <= rightEndTime)) { + throw new Error(__('Invalid period')); + } + + const isOverlapping = leftStartTime < rightEndTime && rightStartTime < leftEndTime; + + if (!isOverlapping) { + return { daysOverlap: 0 }; + } + + const overlapStartDate = Math.max(leftStartTime, rightStartTime); + const overlapEndDate = rightEndTime > leftEndTime ? leftEndTime : rightEndTime; + const differenceInMs = overlapEndDate - overlapStartDate; + + return { + daysOverlap: Math.ceil(differenceInMs / MILLISECONDS_IN_DAY), + overlapStartDate, + overlapEndDate, + }; +}; diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js index 2f19a0c9b26..f11c7658a88 100644 --- a/app/assets/javascripts/lib/utils/dom_utils.js +++ b/app/assets/javascripts/lib/utils/dom_utils.js @@ -6,7 +6,7 @@ import { isInIssuePage, isInMRPage, isInEpicPage } from './common_utils'; * * @param element DOM element to check */ -export const hasHorizontalOverflow = element => +export const hasHorizontalOverflow = (element) => Boolean(element && element.scrollWidth > element.offsetWidth); export const addClassIfElementExists = (element, className) => { @@ -64,7 +64,7 @@ export const parseBooleanDataAttributes = ({ dataset }, names) => * @param {HTMLElement} element The element to test * @returns {Boolean} `true` if the element is currently visible, otherwise false */ -export const isElementVisible = element => +export const isElementVisible = (element) => Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length); /** @@ -76,4 +76,4 @@ export const isElementVisible = element => * @param {HTMLElement} element The element to test * @returns {Boolean} `true` if the element is currently hidden, otherwise false */ -export const isElementHidden = element => !isElementVisible(element); +export const isElementHidden = (element) => !isElementVisible(element); diff --git a/app/assets/javascripts/lib/utils/favicon.js b/app/assets/javascripts/lib/utils/favicon.js new file mode 100644 index 00000000000..47596a76306 --- /dev/null +++ b/app/assets/javascripts/lib/utils/favicon.js @@ -0,0 +1,30 @@ +import { FaviconOverlayManager } from '@gitlab/favicon-overlay'; +import { memoize } from 'lodash'; + +// FaviconOverlayManager is a glorious singleton/static class. Let's start to encapsulate that with this helper. +const getDefaultFaviconManager = memoize(async () => { + await FaviconOverlayManager.initialize({ faviconSelector: '#favicon' }); + + return FaviconOverlayManager; +}); + +export const setFaviconOverlay = async (path) => { + const manager = await getDefaultFaviconManager(); + + manager.setFaviconOverlay(path); +}; + +export const resetFavicon = async () => { + const manager = await getDefaultFaviconManager(); + + manager.resetFaviconOverlay(); +}; + +/** + * Clears the cached memoization of the default manager. + * + * This is needed for determinism in tests. + */ +export const clearMemoizeCache = () => { + getDefaultFaviconManager.cache.clear(); +}; diff --git a/app/assets/javascripts/lib/utils/favicon_ci.js b/app/assets/javascripts/lib/utils/favicon_ci.js new file mode 100644 index 00000000000..613e2620e02 --- /dev/null +++ b/app/assets/javascripts/lib/utils/favicon_ci.js @@ -0,0 +1,16 @@ +import axios from './axios_utils'; +import { setFaviconOverlay, resetFavicon } from './favicon'; + +export const setCiStatusFavicon = (pageUrl) => + axios + .get(pageUrl) + .then(({ data }) => { + if (data && data.favicon) { + return setFaviconOverlay(data.favicon); + } + return resetFavicon(); + }) + .catch((error) => { + resetFavicon(); + throw error; + }); diff --git a/app/assets/javascripts/lib/utils/forms.js b/app/assets/javascripts/lib/utils/forms.js index 1c5f6cefeda..52e1323412d 100644 --- a/app/assets/javascripts/lib/utils/forms.js +++ b/app/assets/javascripts/lib/utils/forms.js @@ -1,14 +1,14 @@ -export const serializeFormEntries = entries => +export const serializeFormEntries = (entries) => entries.reduce((acc, { name, value }) => Object.assign(acc, { [name]: value }), {}); -export const serializeForm = form => { +export const serializeForm = (form) => { const fdata = new FormData(form); - const entries = Array.from(fdata.keys()).map(key => { + const entries = Array.from(fdata.keys()).map((key) => { let val = fdata.getAll(key); // Microsoft Edge has a bug in FormData.getAll() that returns an undefined // value for each form element that does not match the given key: // https://github.com/jimmywarting/FormData/issues/80 - val = val.filter(n => n); + val = val.filter((n) => n); return { name: key, value: val.length === 1 ? val[0] : val }; }); @@ -27,7 +27,7 @@ export const serializeForm = form => { * @example * returns true for '', [], null, undefined */ -export const isEmptyValue = value => value == null || value.length === 0; +export const isEmptyValue = (value) => value == null || value.length === 0; /** * A form object serializer @@ -42,7 +42,7 @@ export const isEmptyValue = value => value == null || value.length === 0; * Returns * {"project": "hello", "username": "john"} */ -export const serializeFormObject = form => +export const serializeFormObject = (form) => Object.fromEntries( Object.entries(form).reduce((acc, [name, { value }]) => { if (!isEmptyValue(value)) { diff --git a/app/assets/javascripts/lib/utils/grammar.js b/app/assets/javascripts/lib/utils/grammar.js index b1f38429369..6d6361d19b6 100644 --- a/app/assets/javascripts/lib/utils/grammar.js +++ b/app/assets/javascripts/lib/utils/grammar.js @@ -16,12 +16,12 @@ import { sprintf, s__ } from '~/locale'; * * @param {String[]} items */ -export const toNounSeriesText = items => { +export const toNounSeriesText = (items, { onlyCommas = false } = {}) => { if (items.length === 0) { return ''; } else if (items.length === 1) { return sprintf(s__(`nounSeries|%{item}`), { item: items[0] }, false); - } else if (items.length === 2) { + } else if (items.length === 2 && !onlyCommas) { return sprintf( s__('nounSeries|%{firstItem} and %{lastItem}'), { @@ -33,7 +33,7 @@ export const toNounSeriesText = items => { } return items.reduce((item, nextItem, idx) => - idx === items.length - 1 + idx === items.length - 1 && !onlyCommas ? sprintf(s__('nounSeries|%{item}, and %{lastItem}'), { item, lastItem: nextItem }, false) : sprintf(s__('nounSeries|%{item}, %{nextItem}'), { item, nextItem }, false), ); diff --git a/app/assets/javascripts/lib/utils/headers.js b/app/assets/javascripts/lib/utils/headers.js new file mode 100644 index 00000000000..80ae3fb146f --- /dev/null +++ b/app/assets/javascripts/lib/utils/headers.js @@ -0,0 +1,3 @@ +export const ContentTypeMultipartFormData = { + 'Content-Type': 'multipart/form-data', +}; diff --git a/app/assets/javascripts/lib/utils/icon_utils.js b/app/assets/javascripts/lib/utils/icon_utils.js index 043043f2eb5..58274092cf8 100644 --- a/app/assets/javascripts/lib/utils/icon_utils.js +++ b/app/assets/javascripts/lib/utils/icon_utils.js @@ -9,7 +9,7 @@ const getSvgDom = memoize(() => axios .get(gon.sprite_icons) .then(({ data: svgs }) => new DOMParser().parseFromString(svgs, 'text/xml')) - .catch(e => { + .catch((e) => { getSvgDom.cache.clear(); throw e; @@ -34,9 +34,9 @@ export const clearSvgIconPathContentCache = () => { * @param {String} name - Icon name * @returns A promise that resolves to the svg path */ -export const getSvgIconPathContent = name => +export const getSvgIconPathContent = (name) => getSvgDom() - .then(doc => { + .then((doc) => { return doc.querySelector(`#${name} path`).getAttribute('d'); }) .catch(() => null); diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js index aa7884846a3..2e92c64ab7a 100644 --- a/app/assets/javascripts/lib/utils/notify.js +++ b/app/assets/javascripts/lib/utils/notify.js @@ -32,7 +32,7 @@ function notifyMe(message, body, icon, onclick) { // If it's okay let's create a notification return notificationGranted(message, opts, onclick); } else if (Notification.permission !== 'denied') { - return Notification.requestPermission(permission => { + return Notification.requestPermission((permission) => { // If the user accepts, let's create a notification if (permission === 'granted') { return notificationGranted(message, opts, onclick); diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js index bc87232f40b..d49382733c0 100644 --- a/app/assets/javascripts/lib/utils/number_utils.js +++ b/app/assets/javascripts/lib/utils/number_utils.js @@ -112,7 +112,7 @@ export const isOdd = (number = 0) => number % 2; * @param {Array} arr An array of numbers * @returns {Number} The median of the given array */ -export const median = arr => { +export const median = (arr) => { const middle = Math.floor(arr.length / 2); const sorted = arr.sort((a, b) => a - b); return arr.length % 2 !== 0 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2; diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js index e8583fa951b..6ec1bd206e6 100644 --- a/app/assets/javascripts/lib/utils/poll.js +++ b/app/assets/javascripts/lib/utils/poll.js @@ -102,11 +102,11 @@ export default class Poll { notificationCallback(true); return resource[method](data) - .then(response => { + .then((response) => { this.checkConditions(response); notificationCallback(false); }) - .catch(error => { + .catch((error) => { notificationCallback(false); if (error.status === httpStatusCodes.ABORTED) { return; diff --git a/app/assets/javascripts/lib/utils/poll_until_complete.js b/app/assets/javascripts/lib/utils/poll_until_complete.js index 199d0e6f0f7..d3b551ca755 100644 --- a/app/assets/javascripts/lib/utils/poll_until_complete.js +++ b/app/assets/javascripts/lib/utils/poll_until_complete.js @@ -29,7 +29,7 @@ export default (url, config = {}) => }, data: { url, config }, method: 'axiosGet', - successCallback: response => { + successCallback: (response) => { if (response.status === httpStatusCodes.OK) { resolve(response); eTagPoll.stop(); diff --git a/app/assets/javascripts/lib/utils/set.js b/app/assets/javascripts/lib/utils/set.js index 541934c4221..a5393bad8c7 100644 --- a/app/assets/javascripts/lib/utils/set.js +++ b/app/assets/javascripts/lib/utils/set.js @@ -5,4 +5,4 @@ * @returns {boolean} */ export const isSubset = (subset, superset) => - Array.from(subset).every(value => superset.has(value)); + Array.from(subset).every((value) => superset.has(value)); diff --git a/app/assets/javascripts/lib/utils/simple_poll.js b/app/assets/javascripts/lib/utils/simple_poll.js index e4e9fb2e6fa..42eb93ea16d 100644 --- a/app/assets/javascripts/lib/utils/simple_poll.js +++ b/app/assets/javascripts/lib/utils/simple_poll.js @@ -4,7 +4,7 @@ export default (fn, { interval = 2000, timeout = 60000 } = {}) => { const startTime = Date.now(); return new Promise((resolve, reject) => { - const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg)); + const stop = (arg) => (arg instanceof Error ? reject(arg) : resolve(arg)); const next = () => { if (timeout === 0 || differenceInMilliseconds(startTime) < timeout) { setTimeout(fn.bind(null, next, stop), interval); diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js index f3244301350..6bb7f09b886 100644 --- a/app/assets/javascripts/lib/utils/sticky.js +++ b/app/assets/javascripts/lib/utils/sticky.js @@ -67,6 +67,6 @@ export const stickyMonitor = (el, stickyTop, insertPlaceholder = true) => { * - If the current environment supports `position: sticky`, do nothing. * - Can receive an iterable element list (NodeList, jQuery collection, etc.) or single HTMLElement. */ -export const polyfillSticky = el => { +export const polyfillSticky = (el) => { StickyFill.add(el); }; diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index c711c0bd163..2c993c8b128 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -14,18 +14,12 @@ function addBlockTags(blockTag, selected) { } function lineBefore(text, textarea) { - const split = text - .substring(0, textarea.selectionStart) - .trim() - .split('\n'); + const split = text.substring(0, textarea.selectionStart).trim().split('\n'); return split[split.length - 1]; } function lineAfter(text, textarea) { - return text - .substring(textarea.selectionEnd) - .trim() - .split('\n')[0]; + return text.substring(textarea.selectionEnd).trim().split('\n')[0]; } function convertMonacoSelectionToAceFormat(sel) { @@ -226,7 +220,7 @@ export function insertMarkdownText({ : blockTagText(text, textArea, blockTag, selected); } else { textToInsert = selectedSplit - .map(val => { + .map((val) => { if (tag.indexOf(textPlaceholder) > -1) { return tag.replace(textPlaceholder, val); } @@ -342,7 +336,7 @@ export function addMarkdownListeners(form) { // eslint-disable-next-line @gitlab/no-global-event-off const $allToolbarBtns = $('.js-md', form) .off('click') - .on('click', function() { + .on('click', function () { const $toolbarBtn = $(this); return updateTextForToolbarBtn($toolbarBtn); @@ -355,7 +349,7 @@ export function addEditorMarkdownListeners(editor) { // eslint-disable-next-line @gitlab/no-global-event-off $('.js-md') .off('click') - .on('click', e => { + .on('click', (e) => { const { mdTag, mdBlock, mdPrepend, mdSelect } = $(e.currentTarget).data(); insertMarkdownText({ diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index c398874db24..eaf396a7a59 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -14,7 +14,7 @@ import { * @param {String} text * @returns {String} */ -export const addDelimiter = text => +export const addDelimiter = (text) => text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') : text; /** @@ -23,7 +23,7 @@ export const addDelimiter = text => * @param {Number} count * @return {Number|String} */ -export const highCountTrim = count => (count > 99 ? '99+' : count); +export const highCountTrim = (count) => (count > 99 ? '99+' : count); /** * Converts first char to uppercase and replaces the given separator with spaces @@ -43,7 +43,7 @@ export const humanize = (string, separator = '_') => { * @param {*} str * @returns {String} */ -export const dasherize = str => str.replace(/[_\s]+/g, '-'); +export const dasherize = (str) => str.replace(/[_\s]+/g, '-'); /** * Replaces whitespace and non-sluggish characters with a given separator @@ -69,7 +69,7 @@ export const slugify = (str, separator = '-') => { * @param {String} str * @returns {String} */ -export const slugifyWithUnderscore = str => slugify(str, '_'); +export const slugifyWithUnderscore = (str) => slugify(str, '_'); /** * Truncates given text @@ -158,7 +158,7 @@ export const truncateWidth = (string, options = {}) => { * @param {String} sha * @returns {String} */ -export const truncateSha = sha => sha.substring(0, 8); +export const truncateSha = (sha) => sha.substring(0, 8); const ELLIPSIS_CHAR = 'ā¦'; export const truncatePathMiddleToLength = (text, maxWidth) => { @@ -166,7 +166,7 @@ export const truncatePathMiddleToLength = (text, maxWidth) => { let ellipsisCount = 0; while (returnText.length >= maxWidth) { - const textSplit = returnText.split('/').filter(s => s !== ELLIPSIS_CHAR); + const textSplit = returnText.split('/').filter((s) => s !== ELLIPSIS_CHAR); if (textSplit.length === 0) { // There are n - 1 path separators for n segments, so 2n - 1 <= maxWidth @@ -243,7 +243,7 @@ export const stripHtml = (string, replace = '') => { * // returns "trailingUnderscore_" * convertToCamelCase('trailing_underscore_') */ -export const convertToCamelCase = string => +export const convertToCamelCase = (string) => string.replace(/([a-z0-9])_([a-z0-9])/gi, (match, p1, p2) => `${p1}${p2.toUpperCase()}`); /** @@ -251,7 +251,7 @@ export const convertToCamelCase = string => * * @param {*} string */ -export const convertToSnakeCase = string => +export const convertToSnakeCase = (string) => slugifyWithUnderscore((string.match(/([a-zA-Z][^A-Z]*)/g) || [string]).join(' ')); /** @@ -260,7 +260,7 @@ export const convertToSnakeCase = string => * * @param {*} string */ -export const convertToSentenceCase = string => { +export const convertToSentenceCase = (string) => { const splitWord = string.split(' ').map((word, index) => (index > 0 ? word.toLowerCase() : word)); return splitWord.join(' '); @@ -273,7 +273,7 @@ export const convertToSentenceCase = string => { * @param {String} string * @returns {String} */ -export const convertToTitleCase = string => string.replace(/\b[a-z]/g, s => s.toUpperCase()); +export const convertToTitleCase = (string) => string.replace(/\b[a-z]/g, (s) => s.toUpperCase()); const unicodeConversion = [ [/[ĆĆĆĆĆ
ÄÄÄ]/g, 'A'], @@ -340,7 +340,7 @@ const unicodeConversion = [ * @param {String} string * @returns {String} */ -export const convertUnicodeToAscii = string => { +export const convertUnicodeToAscii = (string) => { let convertedString = string; unicodeConversion.forEach(([regex, replacer]) => { @@ -356,7 +356,7 @@ export const convertUnicodeToAscii = string => { * * @param {*} string */ -export const splitCamelCase = string => +export const splitCamelCase = (string) => string .replace(/([A-Z]+)([A-Z][a-z])/g, ' $1 $2') .replace(/([a-z\d])([A-Z])/g, '$1 $2') @@ -398,7 +398,7 @@ export const truncateNamespace = (string = '') => { * @param {String} obj The object to test * @returns {Boolean} */ -export const hasContent = obj => isString(obj) && obj.trim() !== ''; +export const hasContent = (obj) => isString(obj) && obj.trim() !== ''; /** * A utility function that validates if a @@ -408,7 +408,7 @@ export const hasContent = obj => isString(obj) && obj.trim() !== ''; * * @return {Boolean} true if valid */ -export const isValidSha1Hash = str => { +export const isValidSha1Hash = (str) => { return /^[0-9a-f]{5,40}$/.test(str); }; diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js index 664c0dbbc84..be1911f7c34 100644 --- a/app/assets/javascripts/lib/utils/type_utility.js +++ b/app/assets/javascripts/lib/utils/type_utility.js @@ -1 +1 @@ -export const isObject = obj => obj && obj.constructor === Object; +export const isObject = (obj) => obj && obj.constructor === Object; diff --git a/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js b/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js index 5d3dd79850e..9d47a1b7132 100644 --- a/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js +++ b/app/assets/javascripts/lib/utils/unit_format/formatter_factory.js @@ -106,7 +106,7 @@ export const scaledSIFormatter = (unit = '', prefixOffset = 0) => { const multiplicative = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; const symbols = [...fractional, '', ...multiplicative]; - const units = symbols.slice(fractional.length + prefixOffset).map(prefix => { + const units = symbols.slice(fractional.length + prefixOffset).map((prefix) => { return `${prefix}${unit}`; }); @@ -126,7 +126,7 @@ export const scaledBinaryFormatter = (unit = '', prefixOffset = 0) => { const multiplicative = ['Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi']; const symbols = ['', ...multiplicative]; - const units = symbols.slice(prefixOffset).map(prefix => { + const units = symbols.slice(prefixOffset).map((prefix) => { return `${prefix}${unit}`; }); diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index a9f6901de32..44d3e78b334 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -112,13 +112,13 @@ export function mergeUrlParams(params, url, options = {}) { const mergedKeys = sort ? Object.keys(merged).sort() : Object.keys(merged); const newQuery = mergedKeys - .filter(key => merged[key] !== null) - .map(key => { + .filter((key) => merged[key] !== null) + .map((key) => { let value = merged[key]; const encodedKey = encodeURIComponent(key); if (spreadArrays && Array.isArray(value)) { value = merged[key] - .map(arrayValue => encodeURIComponent(arrayValue)) + .map((arrayValue) => encodeURIComponent(arrayValue)) .join(`&${encodedKey}[]=`); return `${encodedKey}[]=${value}`; } @@ -150,11 +150,11 @@ export function removeParams(params, url = window.location.href, skipEncoding = return url; } - const removableParams = skipEncoding ? params : params.map(param => encodeURIComponent(param)); + const removableParams = skipEncoding ? params : params.map((param) => encodeURIComponent(param)); const updatedQuery = query .split('&') - .filter(paramPair => { + .filter((paramPair) => { const [foundParam] = paramPair.split('='); return removableParams.indexOf(foundParam) < 0; }) @@ -237,7 +237,7 @@ export function redirectTo(url) { return window.location.assign(url); } -export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/'); +export const escapeFileUrl = (fileUrl) => encodeURIComponent(fileUrl).replace(/%2F/g, '/'); export function webIDEUrl(route = undefined) { let returnUrl = `${gon.relative_url_root || ''}/-/ide/`; @@ -396,7 +396,7 @@ export function queryToObject(query, options = {}) { */ export function objectToQuery(obj) { return Object.keys(obj) - .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(obj[k])}`) + .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(obj[k])}`) .join('&'); } @@ -420,7 +420,7 @@ export const setUrlParams = ( const queryString = urlObj.search; const searchParams = clearParams ? new URLSearchParams('') : new URLSearchParams(queryString); - Object.keys(params).forEach(key => { + Object.keys(params).forEach((key) => { if (params[key] === null || params[key] === undefined) { searchParams.delete(key); } else if (Array.isArray(params[key])) { diff --git a/app/assets/javascripts/lib/utils/users_cache.js b/app/assets/javascripts/lib/utils/users_cache.js index 9f980fd4899..54f69ef8e1b 100644 --- a/app/assets/javascripts/lib/utils/users_cache.js +++ b/app/assets/javascripts/lib/utils/users_cache.js @@ -1,4 +1,4 @@ -import Api from '../../api'; +import { getUsers, getUser, getUserStatus } from '~/rest_api'; import Cache from './cache'; class UsersCache extends Cache { @@ -7,7 +7,7 @@ class UsersCache extends Cache { return Promise.resolve(this.get(username)); } - return Api.users('', { username }).then(({ data }) => { + return getUsers('', { username }).then(({ data }) => { if (!data.length) { throw new Error(`User "${username}" could not be found!`); } @@ -28,7 +28,7 @@ class UsersCache extends Cache { return Promise.resolve(this.get(userId)); } - return Api.user(userId).then(({ data }) => { + return getUser(userId).then(({ data }) => { this.internalStorage[userId] = data; return data; }); @@ -40,7 +40,7 @@ class UsersCache extends Cache { return Promise.resolve(this.get(userId).status); } - return Api.userStatus(userId).then(({ data }) => { + return getUserStatus(userId).then(({ data }) => { if (!this.hasData(userId)) { this.internalStorage[userId] = {}; } diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index 8621a133776..aaa8ee40966 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -1,7 +1,7 @@ /* eslint-disable func-names, no-underscore-dangle, no-param-reassign, consistent-return */ import $ from 'jquery'; -import 'vendor/jquery.scrollTo'; +import { scrollToElement } from '~/lib/utils/common_utils'; // LineHighlighter // @@ -32,7 +32,7 @@ import 'vendor/jquery.scrollTo'; // </div> // -const LineHighlighter = function(options = {}) { +const LineHighlighter = function (options = {}) { options.highlightLineClass = options.highlightLineClass || 'hll'; options.fileHolderSelector = options.fileHolderSelector || '.file-holder'; options.scrollFileHolder = options.scrollFileHolder || false; @@ -50,15 +50,15 @@ const LineHighlighter = function(options = {}) { this.highlightHash(); }; -LineHighlighter.prototype.bindEvents = function() { +LineHighlighter.prototype.bindEvents = function () { const $fileHolder = $(this.options.fileHolderSelector); $fileHolder.on('click', 'a[data-line-number]', this.clickHandler); $fileHolder.on('highlight:line', this.highlightHash); - window.addEventListener('hashchange', e => this.highlightHash(e.target.location.hash)); + window.addEventListener('hashchange', (e) => this.highlightHash(e.target.location.hash)); }; -LineHighlighter.prototype.highlightHash = function(newHash) { +LineHighlighter.prototype.highlightHash = function (newHash) { let range; if (newHash && typeof newHash === 'string') this._hash = newHash; @@ -69,27 +69,21 @@ LineHighlighter.prototype.highlightHash = function(newHash) { if (range[0]) { this.highlightRange(range); const lineSelector = `#L${range[0]}`; - const scrollOptions = { + + scrollToElement(lineSelector, { // Scroll to the first highlighted line on initial load - // Offset -50 for the sticky top bar, and another -100 for some context - offset: -150, - }; - if (this.options.scrollFileHolder) { - $(this.options.fileHolderSelector).scrollTo(lineSelector, scrollOptions); - } else { - $.scrollTo(lineSelector, scrollOptions); - } + // Add an offset of -100 for some context + offset: -100, + }); } } }; -LineHighlighter.prototype.clickHandler = function(event) { +LineHighlighter.prototype.clickHandler = function (event) { let range; event.preventDefault(); this.clearHighlight(); - const lineNumber = $(event.target) - .closest('a') - .data('lineNumber'); + const lineNumber = $(event.target).closest('a').data('lineNumber'); const current = this.hashToRange(this._hash); if (!(current[0] && event.shiftKey)) { // If there's no current selection, or there is but Shift wasn't held, @@ -107,7 +101,7 @@ LineHighlighter.prototype.clickHandler = function(event) { } }; -LineHighlighter.prototype.clearHighlight = function() { +LineHighlighter.prototype.clearHighlight = function () { return $(`.${this.highlightLineClass}`).removeClass(this.highlightLineClass); }; @@ -122,7 +116,7 @@ LineHighlighter.prototype.clearHighlight = function() { // hashToRange('#foo') # => [null, null] // // Returns an Array -LineHighlighter.prototype.hashToRange = function(hash) { +LineHighlighter.prototype.hashToRange = function (hash) { // ?L(\d+)(?:-(\d+))?$/) const matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/); if (matches && matches.length) { @@ -136,14 +130,14 @@ LineHighlighter.prototype.hashToRange = function(hash) { // Highlight a single line // // lineNumber - Line number to highlight -LineHighlighter.prototype.highlightLine = function(lineNumber) { +LineHighlighter.prototype.highlightLine = function (lineNumber) { return $(`#LC${lineNumber}`).addClass(this.highlightLineClass); }; // Highlight all lines within a range // // range - Array containing the starting and ending line numbers -LineHighlighter.prototype.highlightRange = function(range) { +LineHighlighter.prototype.highlightRange = function (range) { if (range[1]) { const results = []; const ref = range[0] <= range[1] ? range : range.reverse(); @@ -158,7 +152,7 @@ LineHighlighter.prototype.highlightRange = function(range) { }; // Set the URL hash string -LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) { +LineHighlighter.prototype.setHash = function (firstLineNumber, lastLineNumber) { let hash; if (lastLineNumber) { hash = `#L${firstLineNumber}-${lastLineNumber}`; @@ -172,7 +166,7 @@ LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) { // Make the actual hash change in the browser // // This method is stubbed in tests. -LineHighlighter.prototype.__setLocationHash__ = function(value) { +LineHighlighter.prototype.__setLocationHash__ = function (value) { return window.history.pushState( { url: value, diff --git a/app/assets/javascripts/locale/ensure_single_line.js b/app/assets/javascripts/locale/ensure_single_line.js index 47c52fe6c50..c2c63777001 100644 --- a/app/assets/javascripts/locale/ensure_single_line.js +++ b/app/assets/javascripts/locale/ensure_single_line.js @@ -18,7 +18,7 @@ module.exports = function ensureSingleLine(str) { if (str.includes('\n') || str.includes('\r')) { return str .split(SPLIT_REGEX) - .filter(s => s !== '') + .filter((s) => s !== '') .join(' '); } return str; diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js index 41aa0f4ddb9..35087b920c7 100644 --- a/app/assets/javascripts/locale/index.js +++ b/app/assets/javascripts/locale/index.js @@ -11,7 +11,7 @@ delete window.translations; @param text The text to be translated @returns {String} The translated text */ -const gettext = text => locale.gettext(ensureSingleLine(text)); +const gettext = (text) => locale.gettext(ensureSingleLine(text)); /** Translate the text with a number @@ -56,7 +56,7 @@ const pgettext = (keyOrContext, key) => { @param formatOptions for available options, please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat @returns {Intl.DateTimeFormat} */ -const createDateTimeFormat = formatOptions => Intl.DateTimeFormat(languageCode(), formatOptions); +const createDateTimeFormat = (formatOptions) => Intl.DateTimeFormat(languageCode(), formatOptions); export { languageCode }; export { gettext as __ }; diff --git a/app/assets/javascripts/locale/sprintf.js b/app/assets/javascripts/locale/sprintf.js index b4658a159d7..82fc816fe9e 100644 --- a/app/assets/javascripts/locale/sprintf.js +++ b/app/assets/javascripts/locale/sprintf.js @@ -15,7 +15,7 @@ export default (input, parameters, escapeParameters = true) => { let output = input; if (parameters) { - Object.keys(parameters).forEach(parameterName => { + Object.keys(parameters).forEach((parameterName) => { const parameterValue = parameters[parameterName]; const escapedParameterValue = escapeParameters ? escape(parameterValue) : parameterValue; output = output.replace(new RegExp(`%{${parameterName}}`, 'g'), escapedParameterValue); diff --git a/app/assets/javascripts/logs/components/environment_logs.vue b/app/assets/javascripts/logs/components/environment_logs.vue index f7c0bd5ae13..a114b3c7d4d 100644 --- a/app/assets/javascripts/logs/components/environment_logs.vue +++ b/app/assets/javascripts/logs/components/environment_logs.vue @@ -246,9 +246,7 @@ export default { </div>{{trace}} </code></pre> </template> - <template #default - ><div></div - ></template> + <template #default><div></div></template> </gl-infinite-scroll> <div ref="logFooter" class="py-2 px-3 text-white bg-secondary-900"> @@ -262,9 +260,7 @@ export default { > <template #fetched>{{ logs.lines.length }}</template> </gl-sprintf> - <template v-else> - {{ s__('Environments|Currently showing all results.') }}</template - > + <template v-else> {{ s__('Environments|Currently showing all results.') }}</template> </div> </div> </template> diff --git a/app/assets/javascripts/logs/components/log_advanced_filters.vue b/app/assets/javascripts/logs/components/log_advanced_filters.vue index 49bb80b3bfd..37fc4dc3735 100644 --- a/app/assets/javascripts/logs/components/log_advanced_filters.vue +++ b/app/assets/javascripts/logs/components/log_advanced_filters.vue @@ -42,7 +42,7 @@ export default { */ podOptions() { if (this.pods.options.length) { - return this.pods.options.map(podName => ({ value: podName, title: podName })); + return this.pods.options.map((podName) => ({ value: podName, title: podName })); } return null; }, diff --git a/app/assets/javascripts/logs/logs_tracking_helper.js b/app/assets/javascripts/logs/logs_tracking_helper.js index 91b0392f71f..26043d646b0 100644 --- a/app/assets/javascripts/logs/logs_tracking_helper.js +++ b/app/assets/javascripts/logs/logs_tracking_helper.js @@ -8,7 +8,7 @@ import Tracking from '~/tracking'; * 3. Change the time range * 4. Use the search bar */ -const trackLogs = label => +const trackLogs = (label) => Tracking.event(document.body.dataset.page, 'logs_view', { label, property: 'count', diff --git a/app/assets/javascripts/logs/stores/actions.js b/app/assets/javascripts/logs/stores/actions.js index 623516f349d..a26e6f694c9 100644 --- a/app/assets/javascripts/logs/stores/actions.js +++ b/app/assets/javascripts/logs/stores/actions.js @@ -11,14 +11,14 @@ const requestUntilData = (url, params) => backOff((next, stop) => { axios .get(url, { params }) - .then(res => { + .then((res) => { if (res.status === httpStatusCodes.ACCEPTED) { next(); return; } stop(res); }) - .catch(err => { + .catch((err) => { stop(err); }); }); @@ -66,12 +66,12 @@ const requestLogsUntilData = ({ commit, state }) => { const filtersToParams = (filters = []) => { // Strings become part of the `search` const search = filters - .filter(f => typeof f === 'string') + .filter((f) => typeof f === 'string') .join(' ') .trim(); // null podName to show all pods - const podName = filters.find(f => f?.type === TOKEN_TYPE_POD_NAME)?.value?.data ?? null; + const podName = filters.find((f) => f?.type === TOKEN_TYPE_POD_NAME)?.value?.data ?? null; return { search, podName }; }; diff --git a/app/assets/javascripts/logs/stores/getters.js b/app/assets/javascripts/logs/stores/getters.js index dc392af8381..836e6e82385 100644 --- a/app/assets/javascripts/logs/stores/getters.js +++ b/app/assets/javascripts/logs/stores/getters.js @@ -3,9 +3,9 @@ import { formatDate } from '../utils'; const mapTrace = ({ timestamp = null, pod = '', message = '' }) => [timestamp ? formatDate(timestamp) : '', pod, message].join(' | '); -export const trace = state => state.logs.lines.map(mapTrace).join('\n'); +export const trace = (state) => state.logs.lines.map(mapTrace).join('\n'); -export const showAdvancedFilters = state => { +export const showAdvancedFilters = (state) => { if (state.environments.current) { const environment = state.environments.options.find( ({ name }) => name === state.environments.current, diff --git a/app/assets/javascripts/logs/utils.js b/app/assets/javascripts/logs/utils.js index 880f762e225..8e21863dd0c 100644 --- a/app/assets/javascripts/logs/utils.js +++ b/app/assets/javascripts/logs/utils.js @@ -22,4 +22,4 @@ export const getTimeRange = (seconds = 0) => { }; }; -export const formatDate = timestamp => dateFormat(timestamp, dateFormatMask); +export const formatDate = (timestamp) => dateFormat(timestamp, dateFormatMask); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index de7648c31b1..ef0fef6085b 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -53,7 +53,7 @@ window.$ = jQuery; jQuery.ajaxSetup({ converters: { // eslint-disable-next-line @gitlab/require-i18n-strings, func-names - 'text script': function(text) { + 'text script': function (text) { jQuery.globalEval(text, { nonce: getCspNonceValue() }); return text; }, @@ -137,9 +137,7 @@ function deferredInitialisation() { $('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() { tooltips.dispose(this); - $(this) - .closest('li') - .addClass('gl-display-none!'); + $(this).closest('li').addClass('gl-display-none!'); }); $('.js-remove-tr').on('ajax:before', function removeTRAjaxBeforeCallback() { @@ -147,9 +145,7 @@ function deferredInitialisation() { }); $('.js-remove-tr').on('ajax:success', function removeTRAjaxSuccessCallback() { - $(this) - .closest('tr') - .addClass('gl-display-none!'); + $(this).closest('tr').addClass('gl-display-none!'); }); const glTooltipDelay = localStorage.getItem('gl-tooltip-delay'); @@ -278,15 +274,9 @@ document.addEventListener('DOMContentLoaded', () => { $this.toggleClass('active'); if ($this.hasClass('active')) { - notesHolders - .show() - .find('.hide, .content') - .show(); + notesHolders.show().find('.hide, .content').show(); } else { - notesHolders - .hide() - .find('.content') - .hide(); + notesHolders.hide().find('.content').hide(); } $(document).trigger('toggle.comments'); @@ -308,7 +298,7 @@ document.addEventListener('DOMContentLoaded', () => { if (flashContainer && flashContainer.children.length) { flashContainer .querySelectorAll('.flash-alert, .flash-notice, .flash-success') - .forEach(flashEl => { + .forEach((flashEl) => { removeFlashClickListener(flashEl); }); } diff --git a/app/assets/javascripts/manual_ordering.js b/app/assets/javascripts/manual_ordering.js index 559efa4c66c..04eaa0c77c3 100644 --- a/app/assets/javascripts/manual_ordering.js +++ b/app/assets/javascripts/manual_ordering.js @@ -39,7 +39,7 @@ const initManualOrdering = (draggableSelector = 'li.issue') => { onStart: () => { sortableStart(); }, - onUpdate: event => { + onUpdate: (event) => { const el = event.item; const url = el.getAttribute('url') || el.dataset.url; diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js index ef7d8cc9efe..a28427eb9ac 100644 --- a/app/assets/javascripts/member_expiration_date.js +++ b/app/assets/javascripts/member_expiration_date.js @@ -24,8 +24,8 @@ export default function memberExpirationDate(selector = '.js-access-expiration-d format: 'yyyy-mm-dd', minDate: new Date(), container: $input.parent().get(0), - parse: dateString => parsePikadayDate(dateString), - toString: date => pikadayToString(date), + parse: (dateString) => parsePikadayDate(dateString), + toString: (date) => pikadayToString(date), onSelect(dateText) { $input.val(calendar.toString(dateText)); @@ -41,9 +41,7 @@ export default function memberExpirationDate(selector = '.js-access-expiration-d inputs.next('.js-clear-input').on('click', function clicked(event) { event.preventDefault(); - const input = $(this) - .closest('.clearable-input') - .find(selector); + const input = $(this).closest('.clearable-input').find(selector); const calendar = input.data('pikaday'); calendar.setDate(null); diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js index 5bd228496da..4cf4fdd12bf 100644 --- a/app/assets/javascripts/members.js +++ b/app/assets/javascripts/members.js @@ -12,13 +12,9 @@ export default class Members { addListeners() { // eslint-disable-next-line @gitlab/no-global-event-off - $('.js-member-update-control') - .off('change') - .on('change', this.formSubmit.bind(this)); + $('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this)); // eslint-disable-next-line @gitlab/no-global-event-off - $('.js-edit-member-form') - .off('ajax:success') - .on('ajax:success', this.formSuccess.bind(this)); + $('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this)); disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change'); } @@ -50,7 +46,7 @@ export default class Members { return $el.data('id'); }, toggleLabel: (selected, $el) => this.dropdownToggleLabel(selected, $el, $btn), - clicked: options => this.dropdownClicked(options), + clicked: (options) => this.dropdownClicked(options), }); }); } diff --git a/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue index 10078d5cd64..fcb70dd45a6 100644 --- a/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue +++ b/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue @@ -28,13 +28,13 @@ export default { if (this.isCurrentUser) { return sprintf( s__('Members|Are you sure you want to withdraw your access request for "%{source}"'), - { source: source.name }, + { source: source.fullName }, ); } return sprintf( s__('Members|Are you sure you want to deny %{usersName}\'s request to join "%{source}"'), - { usersName: user.name, source: source.name }, + { usersName: user.name, source: source.fullName }, ); }, }, diff --git a/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue index 2b0a75640e2..9a27348f146 100644 --- a/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue +++ b/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue @@ -25,7 +25,7 @@ export default { s__( 'Members|Are you sure you want to revoke the invitation for %{inviteEmail} to join "%{source}"', ), - { inviteEmail: invite.email, source: source.name }, + { inviteEmail: invite.email, source: source.fullName }, ); }, }, diff --git a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue index f2bc9c7e876..0e5df961782 100644 --- a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue +++ b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue @@ -36,7 +36,7 @@ export default { s__('Members|Are you sure you want to remove %{usersName} from "%{source}"'), { usersName: user.name, - source: source.name, + source: source.fullName, }, ); } @@ -44,7 +44,7 @@ export default { return sprintf( s__('Members|Are you sure you want to remove this orphaned member from "%{source}"'), { - source: source.name, + source: source.fullName, }, ); }, diff --git a/app/assets/javascripts/members/components/avatars/user_avatar.vue b/app/assets/javascripts/members/components/avatars/user_avatar.vue index fe45ca769af..e2264085e67 100644 --- a/app/assets/javascripts/members/components/avatars/user_avatar.vue +++ b/app/assets/javascripts/members/components/avatars/user_avatar.vue @@ -38,7 +38,7 @@ export default { return this.member.user; }, badges() { - return generateBadges(this.member, this.isCurrentUser).filter(badge => badge.show); + return generateBadges(this.member, this.isCurrentUser).filter((badge) => badge.show); }, statusEmoji() { return this.user?.status?.emoji; diff --git a/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue index c1df0b94234..cf7501d84fa 100644 --- a/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue +++ b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue @@ -45,7 +45,7 @@ export default { computed: { ...mapState(['sourceId', 'filteredSearchBar', 'canManageMembers']), tokens() { - return this.$options.availableTokens.filter(token => { + return this.$options.availableTokens.filter((token) => { if ( Object.prototype.hasOwnProperty.call(token, 'requiredPermissions') && !this[token.requiredPermissions] @@ -61,8 +61,8 @@ export default { const query = queryToObject(window.location.search); const tokens = this.tokens - .filter(token => query[token.type]) - .map(token => ({ + .filter((token) => query[token.type]) + .map((token) => ({ type: token.type, value: { data: query[token.type], diff --git a/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue b/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue index de7fbc4241c..bcfe559768d 100644 --- a/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue +++ b/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue @@ -14,7 +14,7 @@ export default { return parseSortParam(this.tableSortableFields); }, activeOption() { - return FIELDS.find(field => field.key === this.sort.sortByKey); + return FIELDS.find((field) => field.key === this.sort.sortByKey); }, activeOptionLabel() { return this.activeOption?.label; @@ -23,18 +23,18 @@ export default { return !this.sort.sortDesc; }, filteredOptions() { - return FIELDS.filter(field => this.tableSortableFields.includes(field.key) && field.sort).map( - field => ({ - key: field.key, - label: field.label, - href: buildSortHref({ - sortBy: field.key, - sortDesc: false, - filteredSearchBarTokens: this.filteredSearchBar.tokens, - filteredSearchBarSearchParam: this.filteredSearchBar.searchParam, - }), + return FIELDS.filter( + (field) => this.tableSortableFields.includes(field.key) && field.sort, + ).map((field) => ({ + key: field.key, + label: field.label, + href: buildSortHref({ + sortBy: field.key, + sortDesc: false, + filteredSearchBarTokens: this.filteredSearchBar.tokens, + filteredSearchBarSearchParam: this.filteredSearchBar.searchParam, }), - ); + })); }, }, methods: { diff --git a/app/assets/javascripts/members/components/modals/leave_modal.vue b/app/assets/javascripts/members/components/modals/leave_modal.vue index 57a5da774e3..d231c7eabfa 100644 --- a/app/assets/javascripts/members/components/modals/leave_modal.vue +++ b/app/assets/javascripts/members/components/modals/leave_modal.vue @@ -35,7 +35,7 @@ export default { return this.memberPath.replace(/:id$/, 'leave'); }, modalTitle() { - return sprintf(s__('Members|Leave "%{source}"'), { source: this.member.source.name }); + return sprintf(s__('Members|Leave "%{source}"'), { source: this.member.source.fullName }); }, }, methods: { @@ -59,7 +59,7 @@ export default { <gl-form ref="form" :action="leavePath" method="post"> <p> <gl-sprintf :message="$options.modalContent"> - <template #source>{{ member.source.name }}</template> + <template #source>{{ member.source.fullName }}</template> </gl-sprintf> </p> diff --git a/app/assets/javascripts/members/components/table/member_source.vue b/app/assets/javascripts/members/components/table/member_source.vue index 030d72c3420..30fcbfcd3f8 100644 --- a/app/assets/javascripts/members/components/table/member_source.vue +++ b/app/assets/javascripts/members/components/table/member_source.vue @@ -22,6 +22,6 @@ export default { <template> <span v-if="isDirectMember">{{ __('Direct member') }}</span> <a v-else v-gl-tooltip.hover :title="__('Inherited')" :href="memberSource.webUrl">{{ - memberSource.name + memberSource.fullName }}</a> </template> diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue index da77e5caad2..16e0cd5ad4e 100644 --- a/app/assets/javascripts/members/components/table/members_table.vue +++ b/app/assets/javascripts/members/components/table/members_table.vue @@ -34,7 +34,16 @@ export default { computed: { ...mapState(['members', 'tableFields', 'tableAttrs', 'currentUserId', 'sourceId']), filteredFields() { - return FIELDS.filter(field => this.tableFields.includes(field.key) && this.showField(field)); + return FIELDS.filter( + (field) => this.tableFields.includes(field.key) && this.showField(field), + ).map((field) => { + const tdClassFunction = this[field.tdClassFunction]; + + return { + ...field, + ...(tdClassFunction && { tdClass: tdClassFunction }), + }; + }); }, userIsLoggedIn() { return this.currentUserId !== null; @@ -44,6 +53,14 @@ export default { initUserPopovers(this.$el.querySelectorAll('.js-user-link')); }, methods: { + hasActionButtons(member) { + return ( + canRemove(member, this.sourceId) || + canResend(member) || + canUpdate(member, this.currentUserId, this.sourceId) || + canOverride(member) + ); + }, showField(field) { if (!Object.prototype.hasOwnProperty.call(field, 'showFunction')) { return true; @@ -56,14 +73,20 @@ export default { return false; } - return this.members.some(member => { - return ( - canRemove(member, this.sourceId) || - canResend(member) || - canUpdate(member, this.currentUserId, this.sourceId) || - canOverride(member) - ); - }); + return this.members.some((member) => this.hasActionButtons(member)); + }, + tdClassActions(value, key, member) { + if (this.hasActionButtons(member)) { + return 'col-actions'; + } + + return ['col-actions', 'gl-display-none!', 'gl-display-lg-table-cell!']; + }, + tbodyTrAttr(member) { + return { + ...this.tableAttrs.tr, + ...(member?.id && { 'data-testid': `members-table-row-${member.id}` }), + }; }, }, }; @@ -83,7 +106,7 @@ export default { thead-class="border-bottom" :empty-text="__('No members found')" show-empty - :tbody-tr-attr="tableAttrs.tr" + :tbody-tr-attr="tbodyTrAttr" > <template #cell(account)="{ item: member }"> <members-table-cell #default="{ memberType, isCurrentUser }" :member="member"> diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js index 21af825f795..77cb150bff6 100644 --- a/app/assets/javascripts/members/constants.js +++ b/app/assets/javascripts/members/constants.js @@ -70,8 +70,8 @@ export const FIELDS = [ { key: 'actions', thClass: 'col-actions', - tdClass: 'col-actions', showFunction: 'showActionsField', + tdClassFunction: 'tdClassActions', }, ]; diff --git a/app/assets/javascripts/members/store/index.js b/app/assets/javascripts/members/store/index.js index f219f8931b0..34c102999d2 100644 --- a/app/assets/javascripts/members/store/index.js +++ b/app/assets/javascripts/members/store/index.js @@ -2,7 +2,7 @@ import createState from 'ee_else_ce/members/store/state'; import mutations from 'ee_else_ce/members/store/mutations'; import * as actions from 'ee_else_ce/members/store/actions'; -export default initialState => ({ +export default (initialState) => ({ state: createState(initialState), actions, mutations, diff --git a/app/assets/javascripts/members/store/utils.js b/app/assets/javascripts/members/store/utils.js index 7dcd33111e8..585962be27e 100644 --- a/app/assets/javascripts/members/store/utils.js +++ b/app/assets/javascripts/members/store/utils.js @@ -1 +1,2 @@ -export const findMember = (state, memberId) => state.members.find(member => member.id === memberId); +export const findMember = (state, memberId) => + state.members.find((member) => member.id === memberId); diff --git a/app/assets/javascripts/members/utils.js b/app/assets/javascripts/members/utils.js index bf1fc2d7515..780b5a9df57 100644 --- a/app/assets/javascripts/members/utils.js +++ b/app/assets/javascripts/members/utils.js @@ -21,7 +21,7 @@ export const generateBadges = (member, isCurrentUser) => [ }, ]; -export const isGroup = member => { +export const isGroup = (member) => { return Boolean(member.sharedWithGroup); }; @@ -37,7 +37,7 @@ export const canRemove = (member, sourceId) => { return isDirectMember(member, sourceId) && member.canRemove; }; -export const canResend = member => { +export const canResend = (member) => { return Boolean(member.invite?.canResend); }; @@ -47,11 +47,11 @@ export const canUpdate = (member, currentUserId, sourceId) => { ); }; -export const parseSortParam = sortableFields => { +export const parseSortParam = (sortableFields) => { const sortParam = getParameterByName('sort'); - const sortedField = FIELDS.filter(field => sortableFields.includes(field.key)).find( - field => field.sort?.asc === sortParam || field.sort?.desc === sortParam, + const sortedField = FIELDS.filter((field) => sortableFields.includes(field.key)).find( + (field) => field.sort?.asc === sortParam || field.sort?.desc === sortParam, ); if (!sortedField) { @@ -70,7 +70,7 @@ export const buildSortHref = ({ filteredSearchBarTokens, filteredSearchBarSearchParam, }) => { - const sortDefinition = FIELDS.find(field => field.key === sortBy)?.sort; + const sortDefinition = FIELDS.find((field) => field.key === sortBy)?.sort; if (!sortDefinition) { return ''; diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js index 356d8619fed..338fbd9078a 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js @@ -6,7 +6,7 @@ import axios from '~/lib/utils/axios_utils'; import { deprecatedCreateFlash as flash } from '~/flash'; import { __ } from '~/locale'; -(global => { +((global) => { global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts.diffFileEditor = Vue.extend({ @@ -60,19 +60,28 @@ import { __ } from '~/locale'; const DataPromise = axios.get(this.file.content_path); Promise.all([EditorPromise, DataPromise]) - .then(([{ default: EditorLite }, { data: { content, new_path: path } }]) => { - const contentEl = this.$el.querySelector('.editor'); + .then( + ([ + { default: EditorLite }, + { + data: { content, new_path: path }, + }, + ]) => { + const contentEl = this.$el.querySelector('.editor'); - this.originalContent = content; - this.fileLoaded = true; + this.originalContent = content; + this.fileLoaded = true; - this.editor = new EditorLite().createInstance({ - el: contentEl, - blobPath: path, - blobContent: content, - }); - this.editor.onDidChangeModelContent(debounce(this.saveDiffResolution.bind(this), 250)); - }) + this.editor = new EditorLite().createInstance({ + el: contentEl, + blobPath: path, + blobContent: content, + }); + this.editor.onDidChangeModelContent( + debounce(this.saveDiffResolution.bind(this), 250), + ); + }, + ) .catch(() => { flash(__('An error occurred while loading the file')); }); diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js index 827cf5f478d..bc926cb9155 100644 --- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js +++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js @@ -4,7 +4,7 @@ import Vue from 'vue'; import actionsMixin from '../mixins/line_conflict_actions'; import utilsMixin from '../mixins/line_conflict_utils'; -(global => { +((global) => { global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts.inlineConflictLines = Vue.extend({ diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js index 3cb406b819d..bb306e74825 100644 --- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js +++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js @@ -4,7 +4,7 @@ import Vue from 'vue'; import actionsMixin from '../mixins/line_conflict_actions'; import utilsMixin from '../mixins/line_conflict_utils'; -(global => { +((global) => { global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts.parallelConflictLines = Vue.extend({ diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js index c803774f4a7..693f0b619a8 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js @@ -5,7 +5,7 @@ import Vue from 'vue'; import Cookies from 'js-cookie'; import { s__ } from '~/locale'; -(global => { +((global) => { global.mergeConflicts = global.mergeConflicts || {}; const diffViewType = Cookies.get('diff_view'); @@ -48,7 +48,7 @@ import { s__ } from '~/locale'; }, decorateFiles(files) { - files.forEach(file => { + files.forEach((file) => { file.content = ''; file.resolutionData = {}; file.promptDiscardConfirmation = false; @@ -72,7 +72,7 @@ import { s__ } from '~/locale'; setInlineLine(file) { file.inlineLines = []; - file.sections.forEach(section => { + file.sections.forEach((section) => { let currentLineType = 'new'; const { conflict, lines, id } = section; @@ -80,7 +80,7 @@ import { s__ } from '~/locale'; file.inlineLines.push(this.getHeadHeaderLine(id)); } - lines.forEach(line => { + lines.forEach((line) => { const { type } = line; if ((type === 'new' || type === 'old') && currentLineType !== type) { @@ -102,7 +102,7 @@ import { s__ } from '~/locale'; file.parallelLines = []; const linesObj = { left: [], right: [] }; - file.sections.forEach(section => { + file.sections.forEach((section) => { const { conflict, lines, id } = section; if (conflict) { @@ -110,7 +110,7 @@ import { s__ } from '~/locale'; linesObj.right.push(this.getHeadHeaderLine(id)); } - lines.forEach(line => { + lines.forEach((line) => { const { type } = line; if (conflict) { @@ -156,9 +156,9 @@ import { s__ } from '~/locale'; const { files } = this.state.conflictsData; let count = 0; - files.forEach(file => { + files.forEach((file) => { if (file.type === CONFLICT_TYPES.TEXT) { - file.sections.forEach(section => { + file.sections.forEach((section) => { if (section.conflict) { count += 1; } @@ -287,14 +287,14 @@ import { s__ } from '~/locale'; }, restoreFileLinesState(file) { - file.inlineLines.forEach(line => { + file.inlineLines.forEach((line) => { if (line.hasConflict || line.isHeader) { line.isSelected = false; line.isUnselected = false; } }); - file.parallelLines.forEach(lines => { + file.parallelLines.forEach((lines) => { const left = lines[0]; const right = lines[1]; const isLeftMatch = left.hasConflict || left.isHeader; @@ -362,7 +362,7 @@ import { s__ } from '~/locale'; files: [], }; - this.state.conflictsData.files.forEach(file => { + this.state.conflictsData.files.forEach((file) => { const addFile = { old_path: file.old_path, new_path: file.new_path, @@ -388,13 +388,13 @@ import { s__ } from '~/locale'; handleSelected(file, sectionId, selection) { Vue.set(file.resolutionData, sectionId, selection); - file.inlineLines.forEach(line => { + file.inlineLines.forEach((line) => { if (line.id === sectionId && (line.hasConflict || line.isHeader)) { this.markLine(line, selection); } }); - file.parallelLines.forEach(lines => { + file.parallelLines.forEach((lines) => { const left = lines[0]; const right = lines[1]; const hasSameId = right.id === sectionId || left.id === sectionId; @@ -426,7 +426,7 @@ import { s__ } from '~/locale'; }, fileTextTypePresent() { - return this.state.conflictsData.files.some(f => f.type === CONFLICT_TYPES.TEXT); + return this.state.conflictsData.files.some((f) => f.type === CONFLICT_TYPES.TEXT); }, }; })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 344f8dee5ea..bf9e0a309dd 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -31,7 +31,7 @@ function MergeRequest(opts) { fieldName: 'description', selector: '.detail-page-description', lockVersion: this.$el.data('lockVersion'), - onSuccess: result => { + onSuccess: (result) => { document.querySelector('#task_status').innerText = result.task_status; document.querySelector('#task_status_short').innerText = result.task_status_short; }, @@ -47,11 +47,11 @@ function MergeRequest(opts) { } // Local jQuery finder -MergeRequest.prototype.$ = function(selector) { +MergeRequest.prototype.$ = function (selector) { return this.$el.find(selector); }; -MergeRequest.prototype.initTabs = function() { +MergeRequest.prototype.initTabs = function () { if (window.mrTabs) { window.mrTabs.unbindEvents(); } @@ -59,18 +59,18 @@ MergeRequest.prototype.initTabs = function() { window.mrTabs = new MergeRequestTabs(this.opts); }; -MergeRequest.prototype.showAllCommits = function() { +MergeRequest.prototype.showAllCommits = function () { this.$('.first-commits').remove(); return this.$('.all-commits').removeClass('hide'); }; -MergeRequest.prototype.initMRBtnListeners = function() { +MergeRequest.prototype.initMRBtnListeners = function () { const _this = this; const draftToggles = document.querySelectorAll('.js-draft-toggle-button'); if (draftToggles.length) { - draftToggles.forEach(draftToggle => { - draftToggle.addEventListener('click', e => { + draftToggles.forEach((draftToggle) => { + draftToggle.addEventListener('click', (e) => { e.preventDefault(); e.stopImmediatePropagation(); @@ -99,7 +99,7 @@ MergeRequest.prototype.initMRBtnListeners = function() { }); } - return $('.btn-close, .btn-reopen').on('click', function(e) { + return $('.btn-close, .btn-reopen').on('click', function (e) { const $this = $(this); const shouldSubmit = $this.hasClass('btn-comment'); if (shouldSubmit && $this.data('submitted')) { @@ -117,7 +117,7 @@ MergeRequest.prototype.initMRBtnListeners = function() { }); }; -MergeRequest.prototype.submitNoteForm = function(form, $button) { +MergeRequest.prototype.submitNoteForm = function (form, $button) { const noteText = form.find('textarea.js-note-text').val(); if (noteText.trim().length > 0) { form.submit(); @@ -126,8 +126,8 @@ MergeRequest.prototype.submitNoteForm = function(form, $button) { } }; -MergeRequest.prototype.initCommitMessageListeners = function() { - $(document).on('click', 'a.js-with-description-link', e => { +MergeRequest.prototype.initCommitMessageListeners = function () { + $(document).on('click', 'a.js-with-description-link', (e) => { const textarea = $('textarea.js-commit-message'); e.preventDefault(); @@ -136,7 +136,7 @@ MergeRequest.prototype.initCommitMessageListeners = function() { $('.js-without-description-hint').show(); }); - $(document).on('click', 'a.js-without-description-link', e => { + $(document).on('click', 'a.js-without-description-link', (e) => { const textarea = $('textarea.js-commit-message'); e.preventDefault(); @@ -146,7 +146,7 @@ MergeRequest.prototype.initCommitMessageListeners = function() { }); }; -MergeRequest.setStatusBoxToMerged = function() { +MergeRequest.setStatusBoxToMerged = function () { $('.detail-page-header .status-box') .removeClass('status-box-open') .addClass('status-box-mr-merged') @@ -154,20 +154,20 @@ MergeRequest.setStatusBoxToMerged = function() { .text(__('Merged')); }; -MergeRequest.decreaseCounter = function(by = 1) { +MergeRequest.decreaseCounter = function (by = 1) { const $el = $('.js-merge-counter'); const count = Math.max(parseInt($el.text().replace(/[^\d]/, ''), 10) - by, 0); $el.text(addDelimiter(count)); }; -MergeRequest.hideCloseButton = function() { +MergeRequest.hideCloseButton = function () { const el = document.querySelector('.merge-request .js-issuable-actions'); // Dropdown for mobile screen el.querySelector('li.js-close-item').classList.add('hidden'); }; -MergeRequest.toggleDraftStatus = function(title, isReady) { +MergeRequest.toggleDraftStatus = function (title, isReady) { if (isReady) { createFlash(__('The merge request can now be merged.'), 'notice'); } @@ -180,7 +180,7 @@ MergeRequest.toggleDraftStatus = function(title, isReady) { const draftToggles = document.querySelectorAll('.js-draft-toggle-button'); if (draftToggles.length) { - draftToggles.forEach(el => { + draftToggles.forEach((el) => { const draftToggle = el; const url = setUrlParams( { 'merge_request[wip_event]': isReady ? 'wip' : 'unwip' }, diff --git a/app/assets/javascripts/merge_request/components/status_box.vue b/app/assets/javascripts/merge_request/components/status_box.vue new file mode 100644 index 00000000000..fd99802caff --- /dev/null +++ b/app/assets/javascripts/merge_request/components/status_box.vue @@ -0,0 +1,69 @@ +<script> +import { GlIcon } from '@gitlab/ui'; +import { __ } from '~/locale'; +import mrEventHub from '../eventhub'; + +const CLASSES = { + opened: 'status-box-open', + closed: 'status-box-mr-closed', + merged: 'status-box-mr-merged', +}; + +const STATUS = { + opened: [__('Open'), 'issue-open-m'], + closed: [__('Closed'), 'close'], + merged: [__('Merged'), 'git-merge'], +}; + +export default { + components: { + GlIcon, + }, + props: { + initialState: { + type: String, + required: true, + }, + }, + data() { + return { + state: this.initialState, + }; + }, + computed: { + statusBoxClass() { + return CLASSES[this.state]; + }, + statusHumanName() { + return STATUS[this.state][0]; + }, + statusIconName() { + return STATUS[this.state][1]; + }, + }, + created() { + mrEventHub.$on('mr.state.updated', this.updateState); + }, + beforeDestroy() { + mrEventHub.$off('mr.state.updated', this.updateState); + }, + methods: { + updateState({ state }) { + this.state = state; + }, + }, +}; +</script> + +<template> + <div :class="statusBoxClass" class="issuable-status-box status-box"> + <gl-icon + :name="statusIconName" + class="gl-display-block gl-display-sm-none!" + data-testid="status-icon" + /> + <span class="gl-display-none gl-display-sm-block"> + {{ statusHumanName }} + </span> + </div> +</template> diff --git a/app/assets/javascripts/merge_request/eventhub.js b/app/assets/javascripts/merge_request/eventhub.js new file mode 100644 index 00000000000..e31806ad199 --- /dev/null +++ b/app/assets/javascripts/merge_request/eventhub.js @@ -0,0 +1,3 @@ +import createEventHub from '~/helpers/event_hub_factory'; + +export default createEventHub(); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 6e9661ea1a8..81241cd2418 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -1,7 +1,6 @@ /* eslint-disable no-new, class-methods-use-this */ import $ from 'jquery'; -import 'vendor/jquery.scrollTo'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import Cookies from 'js-cookie'; import createEventHub from '~/helpers/event_hub_factory'; @@ -14,6 +13,7 @@ import { handleLocationHash, isMetaClick, parseBoolean, + scrollToElement, } from './lib/utils/common_utils'; import { isInVueNoteablePage } from './lib/utils/dom_utils'; import { getLocationHash } from './lib/utils/url_utility'; @@ -128,7 +128,7 @@ export default class MergeRequestTabs { bindEvents() { $('.merge-request-tabs a[data-toggle="tabvue"]').on('click', this.clickTab); - window.addEventListener('popstate', event => { + window.addEventListener('popstate', (event) => { if (event.state && event.state.action) { this.tabShown(event.state.action, event.target.location); this.currentAction = event.state.action; @@ -177,14 +177,14 @@ export default class MergeRequestTabs { this.currentTab = action; if (this.mergeRequestTabPanesAll) { - this.mergeRequestTabPanesAll.forEach(el => { + this.mergeRequestTabPanesAll.forEach((el) => { const tabPane = el; tabPane.style.display = 'none'; }); } if (this.mergeRequestTabsAll) { - this.mergeRequestTabsAll.forEach(el => { + this.mergeRequestTabsAll.forEach((el) => { el.classList.remove('active'); }); } @@ -255,12 +255,12 @@ export default class MergeRequestTabs { this.eventHub.$emit('MergeRequestTabChange', action); } - scrollToElement(container) { + scrollToContainerElement(container) { if (location.hash) { - const offset = 0 - ($('.navbar-gitlab').outerHeight() + $('.js-tabs-affix').outerHeight()); const $el = $(`${container} ${location.hash}:not(.match)`); + if ($el.length) { - $.scrollTo($el[0], { offset }); + scrollToElement($el[0]); } } } @@ -339,7 +339,7 @@ export default class MergeRequestTabs { document.querySelector('div#commits').innerHTML = data.html; localTimeAgo($('.js-timeago', 'div#commits')); this.commitsLoaded = true; - this.scrollToElement('#commits'); + this.scrollToContainerElement('#commits'); this.toggleLoading(false); initAddContextCommitsTriggers(); @@ -408,7 +408,7 @@ export default class MergeRequestTabs { this.diffsLoaded = true; new Diff(); - this.scrollToElement('#diffs'); + this.scrollToContainerElement('#diffs'); $('.diff-file').each((i, el) => { new BlobForkSuggestion({ diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index 52e9b67c77d..717766578de 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -11,7 +11,7 @@ export default class Milestone { } bindTabsSwitching() { - return $('a[data-toggle="tab"]').on('show.bs.tab', e => { + return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => { const $target = $(e.target); window.location.hash = $target.attr('href'); diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index baa5e41989b..921925e15c5 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -89,7 +89,7 @@ export default class MilestoneSelect { return getMilestones(contextId, reqParams) .then(({ data }) => data - .map(m => ({ + .map((m) => ({ ...m, // Public API includes `title` instead of `name`. name: m.title, @@ -105,7 +105,7 @@ export default class MilestoneSelect { return 0; }), ) - .then(data => { + .then((data) => { const extraOptions = []; if (showAny) { extraOptions.push({ @@ -146,7 +146,7 @@ export default class MilestoneSelect { $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active'); }); }, - renderRow: milestone => { + renderRow: (milestone) => { const milestoneName = milestone.title || milestone.name; let milestoneDisplayName = escape(milestoneName); @@ -178,8 +178,8 @@ export default class MilestoneSelect { }, defaultLabel, fieldName: $dropdown.data('fieldName'), - text: milestone => escape(milestone.title), - id: milestone => { + text: (milestone) => escape(milestone.title), + id: (milestone) => { if (milestone !== undefined) { if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) { return milestone.name; @@ -193,7 +193,7 @@ export default class MilestoneSelect { // display:block overrides the hide-collapse rule return $value.css('display', ''); }, - opened: e => { + opened: (e) => { const $el = $(e.currentTarget); if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) { selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault; @@ -202,7 +202,7 @@ export default class MilestoneSelect { $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active'); }, vue: $dropdown.hasClass('js-issue-board-sidebar'), - clicked: clickEvent => { + clicked: (clickEvent) => { const { e } = clickEvent; let selected = clickEvent.selectedObj; diff --git a/app/assets/javascripts/milestones/components/milestone_combobox.vue b/app/assets/javascripts/milestones/components/milestone_combobox.vue index 08fd5a5994f..1db2d10db20 100644 --- a/app/assets/javascripts/milestones/components/milestone_combobox.vue +++ b/app/assets/javascripts/milestones/components/milestone_combobox.vue @@ -112,7 +112,7 @@ export default { value: { immediate: true, handler() { - const milestoneTitles = this.value.map(milestone => + const milestoneTitles = this.value.map((milestone) => milestone.title ? milestone.title : milestone, ); if (!isEqual(milestoneTitles, this.selectedMilestones)) { diff --git a/app/assets/javascripts/milestones/stores/actions.js b/app/assets/javascripts/milestones/stores/actions.js index df45c7156ad..48fe0dd1e31 100644 --- a/app/assets/javascripts/milestones/stores/actions.js +++ b/app/assets/javascripts/milestones/stores/actions.js @@ -41,10 +41,10 @@ export const fetchProjectMilestones = ({ commit, state }) => { commit(types.REQUEST_START); Api.projectMilestones(state.projectId) - .then(response => { + .then((response) => { commit(types.RECEIVE_PROJECT_MILESTONES_SUCCESS, response); }) - .catch(error => { + .catch((error) => { commit(types.RECEIVE_PROJECT_MILESTONES_ERROR, error); }) .finally(() => { @@ -56,10 +56,10 @@ export const fetchGroupMilestones = ({ commit, state }) => { commit(types.REQUEST_START); Api.groupMilestones(state.groupId) - .then(response => { + .then((response) => { commit(types.RECEIVE_GROUP_MILESTONES_SUCCESS, response); }) - .catch(error => { + .catch((error) => { commit(types.RECEIVE_GROUP_MILESTONES_ERROR, error); }) .finally(() => { @@ -76,10 +76,10 @@ export const searchProjectMilestones = ({ commit, state }) => { commit(types.REQUEST_START); Api.projectSearch(state.projectId, options) - .then(response => { + .then((response) => { commit(types.RECEIVE_PROJECT_MILESTONES_SUCCESS, response); }) - .catch(error => { + .catch((error) => { commit(types.RECEIVE_PROJECT_MILESTONES_ERROR, error); }) .finally(() => { @@ -95,10 +95,10 @@ export const searchGroupMilestones = ({ commit, state }) => { commit(types.REQUEST_START); Api.groupMilestones(state.groupId, options) - .then(response => { + .then((response) => { commit(types.RECEIVE_GROUP_MILESTONES_SUCCESS, response); }) - .catch(error => { + .catch((error) => { commit(types.RECEIVE_GROUP_MILESTONES_ERROR, error); }) .finally(() => { diff --git a/app/assets/javascripts/milestones/stores/mutations.js b/app/assets/javascripts/milestones/stores/mutations.js index 601b88cb62a..3a7babf6fa0 100644 --- a/app/assets/javascripts/milestones/stores/mutations.js +++ b/app/assets/javascripts/milestones/stores/mutations.js @@ -22,7 +22,7 @@ export default { }, [types.REMOVE_SELECTED_MILESTONE](state, selectedMilestone) { const filteredMilestones = state.selectedMilestones.filter( - milestone => milestone !== selectedMilestone, + (milestone) => milestone !== selectedMilestone, ); Vue.set(state, 'selectedMilestones', filteredMilestones); }, diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js index d4283701367..05f2f15fa9a 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js @@ -47,7 +47,7 @@ export default class MiniPipelineGraph { $(document).on( 'click', `${this.container} .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item`, - e => { + (e) => { e.stopPropagation(); }, ); @@ -92,11 +92,7 @@ export default class MiniPipelineGraph { }) .catch(() => { this.toggleLoading(button); - if ( - $(button) - .parent() - .hasClass('open') - ) { + if ($(button).parent().hasClass('open')) { $(button).dropdown('toggle'); } flash(__('An error occurred while fetching the builds.'), 'alert'); diff --git a/app/assets/javascripts/mirrors/mirror_repos.js b/app/assets/javascripts/mirrors/mirror_repos.js index 18ea27e9a34..f7200f22471 100644 --- a/app/assets/javascripts/mirrors/mirror_repos.js +++ b/app/assets/javascripts/mirrors/mirror_repos.js @@ -80,7 +80,7 @@ export default class MirrorRepos { this.debouncedUpdateUrl = debounce(() => this.updateUrl(), 200); this.$urlInput.on('input', () => this.debouncedUpdateUrl()); this.$protectedBranchesInput.on('change', () => this.updateProtectedBranches()); - this.$table.on('click', '.js-delete-mirror', event => this.deleteMirror(event)); + this.$table.on('click', '.js-delete-mirror', (event) => this.deleteMirror(event)); } togglePassword() { diff --git a/app/assets/javascripts/mirrors/ssh_mirror.js b/app/assets/javascripts/mirrors/ssh_mirror.js index c6486350f3b..b692db10e2d 100644 --- a/app/assets/javascripts/mirrors/ssh_mirror.js +++ b/app/assets/javascripts/mirrors/ssh_mirror.js @@ -29,10 +29,10 @@ export default class SSHMirror { this.handleRepositoryUrlInput(true); this.$repositoryUrl.on('keyup', () => this.handleRepositoryUrlInput()); - this.$knownHosts.on('keyup', e => this.handleSSHKnownHostsInput(e)); - this.$dropdownAuthType.on('change', e => this.handleAuthTypeChange(e)); - this.$btnDetectHostKeys.on('click', e => this.handleDetectHostKeys(e)); - this.$btnSSHHostsShowAdvanced.on('click', e => this.handleSSHHostsAdvanced(e)); + this.$knownHosts.on('keyup', (e) => this.handleSSHKnownHostsInput(e)); + this.$dropdownAuthType.on('change', (e) => this.handleAuthTypeChange(e)); + this.$btnDetectHostKeys.on('click', (e) => this.handleDetectHostKeys(e)); + this.$btnSSHHostsShowAdvanced.on('click', (e) => this.handleSSHHostsAdvanced(e)); } /** @@ -100,7 +100,7 @@ export default class SSHMirror { }) .catch(stop); }) - .then(res => { + .then((res) => { $btnLoadSpinner.addClass('d-none'); // Once data is received, we show verification info along with Host keys and fingerprints this.$hostKeysInformation @@ -160,7 +160,7 @@ export default class SSHMirror { showSSHInformation(sshHostKeys) { const $fingerprintsList = this.$hostKeysInformation.find('.js-fingerprints-list'); let fingerprints = ''; - sshHostKeys.fingerprints.forEach(fingerprint => { + sshHostKeys.fingerprints.forEach((fingerprint) => { const escFingerprints = escape(fingerprint.fingerprint); fingerprints += `<code>${escFingerprints}</code>`; }); diff --git a/app/assets/javascripts/monitoring/components/alert_widget.vue b/app/assets/javascripts/monitoring/components/alert_widget.vue index 8f9c181258f..bf31b86561a 100644 --- a/app/assets/javascripts/monitoring/components/alert_widget.vue +++ b/app/assets/javascripts/monitoring/components/alert_widget.vue @@ -97,12 +97,12 @@ export default { return Boolean(this.firingAlerts.length); }, firingAlerts() { - return values(this.alertsToManage).filter(alert => + return values(this.alertsToManage).filter((alert) => this.passedAlertThreshold(this.getQueryData(alert), alert), ); }, formattedFiringAlerts() { - return this.firingAlerts.map(alert => this.formatAlertSummary(alert.alert_path)); + return this.firingAlerts.map((alert) => this.formatAlertSummary(alert.alert_path)); }, configuredAlert() { return this.hasAlerts ? values(this.alertsToManage)[0].metricId : ''; @@ -116,13 +116,13 @@ export default { fetchAlertData() { this.isLoading = true; - const queriesWithAlerts = this.relevantQueries.filter(query => query.alert_path); + const queriesWithAlerts = this.relevantQueries.filter((query) => query.alert_path); return Promise.all( - queriesWithAlerts.map(query => + queriesWithAlerts.map((query) => this.service .readAlert(query.alert_path) - .then(alertAttributes => this.setAlert(alertAttributes, query.metricId)), + .then((alertAttributes) => this.setAlert(alertAttributes, query.metricId)), ), ) .then(() => { @@ -141,7 +141,7 @@ export default { }, formatAlertSummary(alertPath) { const alert = this.alertsToManage[alertPath]; - const alertQuery = this.relevantQueries.find(query => query.metricId === alert.metricId); + const alertQuery = this.relevantQueries.find((query) => query.metricId === alert.metricId); return `${alertQuery.label} ${alert.operator} ${alert.threshold}`; }, @@ -150,19 +150,19 @@ export default { switch (operator) { case OPERATORS.greaterThan: - return data.some(value => value > threshold); + return data.some((value) => value > threshold); case OPERATORS.lessThan: - return data.some(value => value < threshold); + return data.some((value) => value < threshold); case OPERATORS.equalTo: - return data.some(value => value === threshold); + return data.some((value) => value === threshold); default: return false; } }, getQueryData(alert) { - const alertQuery = this.relevantQueries.find(query => query.metricId === alert.metricId); + const alertQuery = this.relevantQueries.find((query) => query.metricId === alert.metricId); - return get(alertQuery, 'result[0].values', []).map(value => get(value, '[1]', null)); + return get(alertQuery, 'result[0].values', []).map((value) => get(value, '[1]', null)); }, showModal() { this.$root.$emit('bv::show::modal', this.modalId); @@ -179,7 +179,7 @@ export default { this.isLoading = true; this.service .createAlert(newAlert) - .then(alertAttributes => { + .then((alertAttributes) => { this.setAlert(alertAttributes, prometheus_metric_id); this.isLoading = false; this.hideModal(); @@ -194,7 +194,7 @@ export default { this.isLoading = true; this.service .updateAlert(alert, updatedAlert) - .then(alertAttributes => { + .then((alertAttributes) => { this.setAlert(alertAttributes, this.alertsToManage[alert].metricId); this.isLoading = false; this.hideModal(); diff --git a/app/assets/javascripts/monitoring/components/alert_widget_form.vue b/app/assets/javascripts/monitoring/components/alert_widget_form.vue index 71691429ece..b26941891e4 100644 --- a/app/assets/javascripts/monitoring/components/alert_widget_form.vue +++ b/app/assets/javascripts/monitoring/components/alert_widget_form.vue @@ -99,7 +99,7 @@ export default { return this.alertQuery.length ? true : null; }, currentQuery() { - return this.relevantQueries.find(query => query.metricId === this.prometheusMetricId) || {}; + return this.relevantQueries.find((query) => query.metricId === this.prometheusMetricId) || {}; }, formDisabled() { // We need a prometheusMetricId to determine whether we're @@ -151,7 +151,7 @@ export default { }, methods: { selectQuery(queryId) { - const existingAlertPath = findKey(this.alertsToManage, alert => alert.metricId === queryId); + const existingAlertPath = findKey(this.alertsToManage, (alert) => alert.metricId === queryId); const existingAlert = this.alertsToManage[existingAlertPath]; if (existingAlert) { diff --git a/app/assets/javascripts/monitoring/components/charts/annotations.js b/app/assets/javascripts/monitoring/components/charts/annotations.js index 418107c4126..aac9d2f8a01 100644 --- a/app/assets/javascripts/monitoring/components/charts/annotations.js +++ b/app/assets/javascripts/monitoring/components/charts/annotations.js @@ -52,7 +52,7 @@ export const annotationsYAxis = { * @param {Object} annotation object * @returns {Object} markLine object */ -export const parseAnnotations = annotations => +export const parseAnnotations = (annotations) => annotations.reduce( (acc, annotation) => { acc.lines.push({ @@ -87,7 +87,7 @@ export const parseAnnotations = annotations => */ export const generateAnnotationsSeries = ({ deployments = [], annotations = [] } = {}) => { // deployment data points - const data = deployments.map(deployment => { + const data = deployments.map((deployment) => { return { name: 'deployments', value: [deployment.createdAt, annotationsYAxisCoords.pos], diff --git a/app/assets/javascripts/monitoring/components/charts/anomaly.vue b/app/assets/javascripts/monitoring/components/charts/anomaly.vue index ac401c6e381..cb533c38fa0 100644 --- a/app/assets/javascripts/monitoring/components/charts/anomaly.vue +++ b/app/assets/javascripts/monitoring/components/charts/anomaly.vue @@ -1,4 +1,5 @@ <script> +import produce from 'immer'; import { flattenDeep, isNumber } from 'lodash'; import { GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; import { roundOffFloat } from '~/lib/utils/common_utils'; @@ -61,7 +62,7 @@ export default { }, computed: { series() { - return this.graphData.metrics.map(metric => { + return this.graphData.metrics.map((metric) => { const values = metric.result && metric.result[0] ? metric.result[0].values : []; return { label: metric.label, @@ -77,18 +78,20 @@ export default { * This offset is the lowest value. */ yOffset() { - const values = flattenDeep(this.series.map(ser => ser.data.map(([, y]) => y))); + const values = flattenDeep(this.series.map((ser) => ser.data.map(([, y]) => y))); const min = values.length ? Math.floor(Math.min(...values)) : 0; return min < 0 ? -min : 0; }, metricData() { const originalMetricQuery = this.graphData.metrics[0]; - const metricQuery = { ...originalMetricQuery }; - metricQuery.result[0].values = metricQuery.result[0].values.map(([x, y]) => [ - x, - y + this.yOffset, - ]); + const metricQuery = produce(originalMetricQuery, (draftQuery) => { + // eslint-disable-next-line no-param-reassign + draftQuery.result[0].values = draftQuery.result[0].values.map(([x, y]) => [ + x, + y + this.yOffset, + ]); + }); return { ...this.graphData, type: panelTypes.LINE_CHART, @@ -109,7 +112,7 @@ export default { }, showSymbol: true, itemStyle: { - color: params => { + color: (params) => { if (this.isDatapointAnomaly(params.dataIndex)) { return colorValues.anomalySymbol; } @@ -128,7 +131,7 @@ export default { const yAxisWithOffset = { axisLabel: { - formatter: num => roundOffFloat(num - this.yOffset, 3).toString(), + formatter: (num) => roundOffFloat(num - this.yOffset, 3).toString(), }, }; @@ -153,7 +156,7 @@ export default { boundarySeries.push( this.makeBoundarySeries({ name: this.formatLegendLabel(upperSeries), - data: calcOffsetY(upperSeries.data, i => -this.yValue(LOWER, i)), + data: calcOffsetY(upperSeries.data, (i) => -this.yValue(LOWER, i)), areaStyle: { color: AREA_COLOR, opacity: AREA_OPACITY, diff --git a/app/assets/javascripts/monitoring/components/charts/bar.vue b/app/assets/javascripts/monitoring/components/charts/bar.vue index e1018cd5952..a4cef5ea256 100644 --- a/app/assets/javascripts/monitoring/components/charts/bar.vue +++ b/app/assets/javascripts/monitoring/components/charts/bar.vue @@ -67,12 +67,12 @@ export default { }, setSvg(name) { getSvgIconPathContent(name) - .then(path => { + .then((path) => { if (path) { this.$set(this.svgs, name, `path://${path}`); } }) - .catch(e => { + .catch((e) => { // eslint-disable-next-line no-console, @gitlab/require-i18n-strings console.error('SVG could not be rendered correctly: ', e); }); diff --git a/app/assets/javascripts/monitoring/components/charts/column.vue b/app/assets/javascripts/monitoring/components/charts/column.vue index 511f77a441b..ba947c2fa9c 100644 --- a/app/assets/javascripts/monitoring/components/charts/column.vue +++ b/app/assets/javascripts/monitoring/components/charts/column.vue @@ -90,7 +90,7 @@ export default { }, setSvg(name) { getSvgIconPathContent(name) - .then(path => { + .then((path) => { if (path) { this.$set(this.svgs, name, `path://${path}`); } diff --git a/app/assets/javascripts/monitoring/components/charts/heatmap.vue b/app/assets/javascripts/monitoring/components/charts/heatmap.vue index 7003e2d37cf..22214a76aba 100644 --- a/app/assets/javascripts/monitoring/components/charts/heatmap.vue +++ b/app/assets/javascripts/monitoring/components/charts/heatmap.vue @@ -42,10 +42,10 @@ export default { return this.graphData.y_label || ''; }, xAxisLabels() { - return this.metrics.result.map(res => Object.values(res.metric)[0]); + return this.metrics.result.map((res) => Object.values(res.metric)[0]); }, yAxisLabels() { - return this.result.values.map(val => { + return this.result.values.map((val) => { const [yLabel] = val; return formatDate(new Date(yLabel), { diff --git a/app/assets/javascripts/monitoring/components/charts/options.js b/app/assets/javascripts/monitoring/components/charts/options.js index 0cd4a02311c..163a7be6973 100644 --- a/app/assets/javascripts/monitoring/components/charts/options.js +++ b/app/assets/javascripts/monitoring/components/charts/options.js @@ -51,7 +51,7 @@ const getDataAxisOptions = ({ format, precision, name }) => { nameLocation: 'center', // same as gitlab-ui's default scale: true, axisLabel: { - formatter: val => formatter(val, precision, maxDataAxisTickLength), + formatter: (val) => formatter(val, precision, maxDataAxisTickLength), }, }; }; @@ -85,7 +85,7 @@ export const getTimeAxisOptions = ({ name: __('Time'), type: axisTypes.time, axisLabel: { - formatter: date => formatDate(date, { format, timezone }), + formatter: (date) => formatDate(date, { format, timezone }), }, axisPointer: { snap: false, @@ -109,7 +109,7 @@ export const getTooltipFormatter = ({ precision = defaultTooltipPrecision, } = {}) => { const formatter = getFormatter(format); - return num => formatter(num, precision); + return (num) => formatter(num, precision); }; // Thresholds @@ -138,9 +138,9 @@ export const getValidThresholds = ({ mode, range = {}, values = [] } = {}) => { const uniqueThresholds = uniq(values); - const numberThresholds = uniqueThresholds.filter(threshold => isFinite(threshold)); + const numberThresholds = uniqueThresholds.filter((threshold) => isFinite(threshold)); - const validThresholds = numberThresholds.filter(threshold => { + const validThresholds = numberThresholds.filter((threshold) => { let isValid; if (mode === thresholdModeTypes.PERCENTAGE) { @@ -152,7 +152,7 @@ export const getValidThresholds = ({ mode, range = {}, values = [] } = {}) => { return isValid; }); - const transformedThresholds = validThresholds.map(threshold => { + const transformedThresholds = validThresholds.map((threshold) => { let transformedThreshold; if (mode === 'percentage') { diff --git a/app/assets/javascripts/monitoring/components/charts/stacked_column.vue b/app/assets/javascripts/monitoring/components/charts/stacked_column.vue index 66b4d0d86e6..b5ae6bcfd13 100644 --- a/app/assets/javascripts/monitoring/components/charts/stacked_column.vue +++ b/app/assets/javascripts/monitoring/components/charts/stacked_column.vue @@ -68,7 +68,7 @@ export default { if (!result || result.length === 0) { return []; } - return { name, data: result[0].values.map(val => val[1]) }; + return { name, data: result[0].values.map((val) => val[1]) }; }) .slice(0, 1); }, @@ -89,7 +89,7 @@ export default { if (!result || result.length === 0) { return []; } - return result[0].values.map(val => val[0]); + return result[0].values.map((val) => val[0]); }, dataZoomConfig() { const handleIcon = this.svgs['scroll-handle']; @@ -106,7 +106,7 @@ export default { }; }, seriesNames() { - return this.graphData.metrics.map(metric => metric.label); + return this.graphData.metrics.map((metric) => metric.label); }, }, created() { @@ -115,12 +115,12 @@ export default { methods: { setSvg(name) { getSvgIconPathContent(name) - .then(path => { + .then((path) => { if (path) { this.$set(this.svgs, name, `path://${path}`); } }) - .catch(e => { + .catch((e) => { // eslint-disable-next-line no-console, @gitlab/require-i18n-strings console.error('SVG could not be rendered correctly: ', e); }); diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue index 170c5ff7695..e9f7b11c977 100644 --- a/app/assets/javascripts/monitoring/components/charts/time_series.vue +++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue @@ -11,7 +11,7 @@ import { makeDataSeries } from '~/helpers/monitor_helper'; import { graphDataValidatorForValues } from '../../utils'; import { formatDate, timezones } from '../../format_date'; -export const timestampToISODate = timestamp => new Date(timestamp).toISOString(); +export const timestampToISODate = (timestamp) => new Date(timestamp).toISOString(); const THROTTLED_DATAZOOM_WAIT = 1000; // milliseconds @@ -304,7 +304,7 @@ export default { this.tooltip.content = []; - params.seriesData.forEach(dataPoint => { + params.seriesData.forEach((dataPoint) => { if (dataPoint.value) { const [, yVal] = dataPoint.value; this.tooltip.type = dataPoint.name; @@ -327,12 +327,12 @@ export default { }, setSvg(name) { getSvgIconPathContent(name) - .then(path => { + .then((path) => { if (path) { this.$set(this.svgs, name, `path://${path}`); } }) - .catch(e => { + .catch((e) => { // eslint-disable-next-line no-console, @gitlab/require-i18n-strings console.error('SVG could not be rendered correctly: ', e); }); diff --git a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue index 070277fe2dc..9d1926dca54 100644 --- a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue +++ b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue @@ -1,7 +1,7 @@ <script> import { mapState, mapGetters, mapActions } from 'vuex'; import { - GlDeprecatedButton, + GlButton, GlDropdown, GlDropdownDivider, GlDropdownItem, @@ -22,7 +22,7 @@ import { getAddMetricTrackingOptions } from '../utils'; export default { components: { - GlDeprecatedButton, + GlButton, GlDropdown, GlDropdownDivider, GlDropdownItem, @@ -178,10 +178,10 @@ export default { /> </form> <div slot="modal-footer"> - <gl-deprecated-button @click="hideAddMetricModal"> + <gl-button @click="hideAddMetricModal"> {{ __('Cancel') }} - </gl-deprecated-button> - <gl-deprecated-button + </gl-button> + <gl-button v-track-event="getAddMetricTrackingOptions()" data-testid="add-metric-modal-submit-button" :disabled="!customMetricsFormIsValid" @@ -189,7 +189,7 @@ export default { @click="submitCustomMetricsForm" > {{ __('Save changes') }} - </gl-deprecated-button> + </gl-button> </div> </gl-modal> </template> diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue index ad7127d97de..2b0c3d03b8d 100644 --- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue +++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue @@ -271,8 +271,8 @@ export default { methods: { getGraphAlerts(queries) { if (!this.allAlerts) return {}; - const metricIdsForChart = queries.map(q => q.metricId); - return pickBy(this.allAlerts, alert => metricIdsForChart.includes(alert.metricId)); + const metricIdsForChart = queries.map((q) => q.metricId); + return pickBy(this.allAlerts, (alert) => metricIdsForChart.includes(alert.metricId)); }, getGraphAlertValues(queries) { return Object.values(this.getGraphAlerts(queries)); @@ -346,10 +346,10 @@ export default { } }, getAlertRunbooks(queries) { - const hasRunbook = alert => Boolean(alert.runbookUrl); + const hasRunbook = (alert) => Boolean(alert.runbookUrl); const graphAlertsWithRunbooks = pickBy(this.getGraphAlerts(queries), hasRunbook); - const alertToRunbookTransform = alert => { - const alertQuery = queries.find(query => query.metricId === alert.metricId); + const alertToRunbookTransform = (alert) => { + const alertQuery = queries.find((query) => query.metricId === alert.metricId); return { key: alert.metricId, href: alert.runbookUrl, diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue b/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue index 0a1b1cd2c08..bcfa1b04322 100644 --- a/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue +++ b/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue @@ -146,7 +146,7 @@ export default { ) " > - <template #code="{content}"> + <template #code="{ content }"> <code>{{ content }}</code> </template> </gl-sprintf> diff --git a/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue b/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue index db5b853d451..627af202028 100644 --- a/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue +++ b/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue @@ -3,7 +3,7 @@ import { GlFormGroup, GlFormInput, GlFormRadioGroup, GlFormTextarea } from '@git import { escape as esc } from 'lodash'; import { __, s__, sprintf } from '~/locale'; -const defaultFileName = dashboard => dashboard.path.split('/').reverse()[0]; +const defaultFileName = (dashboard) => dashboard.path.split('/').reverse()[0]; export default { components: { diff --git a/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue b/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue index e64afc01fd9..b87934a1db2 100644 --- a/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue +++ b/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue @@ -42,7 +42,7 @@ export default { this.loading = true; this.alert = null; this.duplicateSystemDashboard(this.form) - .then(createdDashboard => { + .then((createdDashboard) => { this.loading = false; this.alert = null; @@ -55,7 +55,7 @@ export default { this.form.branch === this.defaultBranch ? createdDashboard : this.selectedDashboard; this.$emit(events.dashboardDuplicated, dashboard); }) - .catch(error => { + .catch((error) => { this.loading = false; this.alert = error; }); diff --git a/app/assets/javascripts/monitoring/components/embeds/embed_group.vue b/app/assets/javascripts/monitoring/components/embeds/embed_group.vue index 481ba3636cb..c114ae1809f 100644 --- a/app/assets/javascripts/monitoring/components/embeds/embed_group.vue +++ b/app/assets/javascripts/monitoring/components/embeds/embed_group.vue @@ -17,7 +17,7 @@ export default { urls: { type: Array, required: true, - validator: urls => urls.length > 0, + validator: (urls) => urls.length > 0, }, }, data() { diff --git a/app/assets/javascripts/monitoring/components/embeds/metric_embed.vue b/app/assets/javascripts/monitoring/components/embeds/metric_embed.vue index 1557a49137e..2fe49152c4f 100644 --- a/app/assets/javascripts/monitoring/components/embeds/metric_embed.vue +++ b/app/assets/javascripts/monitoring/components/embeds/metric_embed.vue @@ -101,7 +101,7 @@ export default { }, }), chartHasData(chart) { - return chart.metrics.some(metric => this.metricsWithData.includes(metric.metricId)); + return chart.metrics.some((metric) => this.metricsWithData.includes(metric.metricId)); }, onSidebarMutation() { setTimeout(() => { diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue index 5e7c9b5d906..867f7139d71 100644 --- a/app/assets/javascripts/monitoring/components/empty_state.vue +++ b/app/assets/javascripts/monitoring/components/empty_state.vue @@ -12,7 +12,7 @@ export default { selectedState: { type: String, required: true, - validator: state => Object.values(dashboardEmptyStates).includes(state), + validator: (state) => Object.values(dashboardEmptyStates).includes(state), }, documentationPath: { type: String, diff --git a/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue b/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue index 4e48292c48d..ff0327f5f99 100644 --- a/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue +++ b/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue @@ -28,7 +28,7 @@ export default { }, computed: { text() { - const selectedOpt = this.options.values?.find(opt => opt.value === this.value); + const selectedOpt = this.options.values?.find((opt) => opt.value === this.value); return selectedOpt?.text || this.value; }, }, diff --git a/app/assets/javascripts/monitoring/csv_export.js b/app/assets/javascripts/monitoring/csv_export.js index 20cfa23e9b4..eaeed4a54d4 100644 --- a/app/assets/javascripts/monitoring/csv_export.js +++ b/app/assets/javascripts/monitoring/csv_export.js @@ -66,8 +66,8 @@ const csvMetricHeaders = (axisLabel, metrics) => * * @param {Array} metrics - Metrics with results */ -const csvMetricValues = metrics => - metrics.flatMap(({ result }) => result.map(res => res.values || [])); +const csvMetricValues = (metrics) => + metrics.flatMap(({ result }) => result.map((res) => res.values || [])); /** * Returns headers and rows for csv, sorted by their timestamp. @@ -99,7 +99,7 @@ const csvData = (metricHeaders, metricValues) => { const rows = Object.keys(rowsByTimestamp) .sort() - .map(timestamp => { + .map((timestamp) => { // force each row to have the same number of entries rowsByTimestamp[timestamp].length = metricHeaders.length; // add timestamp as the first entry @@ -111,7 +111,7 @@ const csvData = (metricHeaders, metricValues) => { // appearing inside a field must be escaped by preceding it with // another double quote." // https://tools.ietf.org/html/rfc4180#page-2 - const headers = metricHeaders.map(header => `"${header.replace(/"/g, '""')}"`); + const headers = metricHeaders.map((header) => `"${header.replace(/"/g, '""')}"`); return { headers: ['timestamp', ...headers], @@ -125,12 +125,12 @@ const csvData = (metricHeaders, metricValues) => { * @param {Object} graphData - Panel contents * @returns {String} */ -export const graphDataToCsv = graphData => { +export const graphDataToCsv = (graphData) => { const delimiter = ','; const br = '\r\n'; const { metrics = [], y_label: axisLabel } = graphData; - const metricsWithResults = metrics.filter(metric => metric.result); + const metricsWithResults = metrics.filter((metric) => metric.result); const metricHeaders = csvMetricHeaders(axisLabel, metricsWithResults); const metricValues = csvMetricValues(metricsWithResults); const { headers, rows } = csvData(metricHeaders, metricValues); @@ -140,7 +140,7 @@ export const graphDataToCsv = graphData => { } const headerLine = headers.join(delimiter) + br; - const lines = rows.map(row => row.join(delimiter)); + const lines = rows.map((row) => row.join(delimiter)); return headerLine + lines.join(br) + br; }; diff --git a/app/assets/javascripts/monitoring/requests/index.js b/app/assets/javascripts/monitoring/requests/index.js index 28064361768..4a12ca06197 100644 --- a/app/assets/javascripts/monitoring/requests/index.js +++ b/app/assets/javascripts/monitoring/requests/index.js @@ -3,10 +3,10 @@ import statusCodes from '~/lib/utils/http_status'; import { backOff } from '~/lib/utils/common_utils'; import { PROMETHEUS_TIMEOUT } from '../constants'; -const cancellableBackOffRequest = makeRequestCallback => +const cancellableBackOffRequest = (makeRequestCallback) => backOff((next, stop) => { makeRequestCallback() - .then(resp => { + .then((resp) => { if (resp.status === statusCodes.NO_CONTENT) { next(); } else { @@ -16,19 +16,19 @@ const cancellableBackOffRequest = makeRequestCallback => // If the request is cancelled by axios // then consider it as noop so that its not // caught by subsequent catches - .catch(thrown => (axios.isCancel(thrown) ? undefined : stop(thrown))); + .catch((thrown) => (axios.isCancel(thrown) ? undefined : stop(thrown))); }, PROMETHEUS_TIMEOUT); export const getDashboard = (dashboardEndpoint, params) => cancellableBackOffRequest(() => axios.get(dashboardEndpoint, { params })).then( - axiosResponse => axiosResponse.data, + (axiosResponse) => axiosResponse.data, ); export const getPrometheusQueryData = (prometheusEndpoint, params, opts) => cancellableBackOffRequest(() => axios.get(prometheusEndpoint, { params, ...opts })) - .then(axiosResponse => axiosResponse.data) - .then(prometheusResponse => prometheusResponse.data) - .catch(error => { + .then((axiosResponse) => axiosResponse.data) + .then((prometheusResponse) => prometheusResponse.data) + .catch((error) => { // Prometheus returns errors in specific cases // https://prometheus.io/docs/prometheus/latest/querying/api/#format-overview const { response = {} } = error; diff --git a/app/assets/javascripts/monitoring/services/alerts_service.js b/app/assets/javascripts/monitoring/services/alerts_service.js index a67675f1a3d..cb6dac7aa15 100644 --- a/app/assets/javascripts/monitoring/services/alerts_service.js +++ b/app/assets/javascripts/monitoring/services/alerts_service.js @@ -10,7 +10,7 @@ export default class AlertsService { } getAlerts() { - return axios.get(this.alertsEndpoint).then(resp => mapAlert(resp.data)); + return axios.get(this.alertsEndpoint).then((resp) => mapAlert(resp.data)); } createAlert({ prometheus_metric_id, operator, threshold, runbookUrl }) { @@ -21,23 +21,23 @@ export default class AlertsService { threshold, runbook_url: runbookUrl, }) - .then(resp => mapAlert(resp.data)); + .then((resp) => mapAlert(resp.data)); } // eslint-disable-next-line class-methods-use-this readAlert(alertPath) { - return axios.get(alertPath).then(resp => mapAlert(resp.data)); + return axios.get(alertPath).then((resp) => mapAlert(resp.data)); } // eslint-disable-next-line class-methods-use-this updateAlert(alertPath, { operator, threshold, runbookUrl }) { return axios .put(alertPath, { operator, threshold, runbook_url: runbookUrl }) - .then(resp => mapAlert(resp.data)); + .then((resp) => mapAlert(resp.data)); } // eslint-disable-next-line class-methods-use-this deleteAlert(alertPath) { - return axios.delete(alertPath).then(resp => resp.data); + return axios.delete(alertPath).then((resp) => resp.data); } } diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index e7391a4c9d1..44c200cdb54 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -114,7 +114,7 @@ export const fetchDashboard = ({ state, commit, dispatch, getters }) => { } return getDashboard(state.dashboardEndpoint, params) - .then(response => { + .then((response) => { dispatch('receiveMetricsDashboardSuccess', { response }); /** * After the dashboard is fetched, there can be non-blocking invalid syntax @@ -125,7 +125,7 @@ export const fetchDashboard = ({ state, commit, dispatch, getters }) => { */ dispatch('fetchDashboardValidationWarnings'); }) - .catch(error => { + .catch((error) => { Sentry.captureException(error); commit(types.SET_ALL_DASHBOARDS, error.response?.data?.all_dashboards ?? []); @@ -185,9 +185,9 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => { dispatch('fetchVariableMetricLabelValues', { defaultQueryParams }); const promises = []; - state.dashboard.panelGroups.forEach(group => { - group.panels.forEach(panel => { - panel.metrics.forEach(metric => { + state.dashboard.panelGroups.forEach((group) => { + group.panels.forEach((panel) => { + panel.metrics.forEach((metric) => { promises.push(dispatch('fetchPrometheusMetric', { metric, defaultQueryParams })); }); }); @@ -231,10 +231,10 @@ export const fetchPrometheusMetric = ( commit(types.REQUEST_METRIC_RESULT, { metricId: metric.metricId }); return getPrometheusQueryData(metric.prometheusEndpointPath, queryParams) - .then(data => { + .then((data) => { commit(types.RECEIVE_METRIC_RESULT_SUCCESS, { metricId: metric.metricId, data }); }) - .catch(error => { + .catch((error) => { Sentry.captureException(error); commit(types.RECEIVE_METRIC_RESULT_FAILURE, { metricId: metric.metricId, error }); @@ -251,15 +251,15 @@ export const fetchDeploymentsData = ({ state, dispatch }) => { } return axios .get(state.deploymentsEndpoint) - .then(resp => resp.data) - .then(response => { + .then((resp) => resp.data) + .then((response) => { if (!response || !response.deployments) { createFlash(s__('Metrics|Unexpected deployment data response from prometheus endpoint')); } dispatch('receiveDeploymentsDataSuccess', response.deployments); }) - .catch(error => { + .catch((error) => { Sentry.captureException(error); dispatch('receiveDeploymentsDataFailure'); createFlash(s__('Metrics|There was an error getting deployment information.')); @@ -285,10 +285,10 @@ export const fetchEnvironmentsData = ({ state, dispatch }) => { states: [ENVIRONMENT_AVAILABLE_STATE], }, }) - .then(resp => + .then((resp) => parseEnvironmentsResponse(resp.data?.project?.data?.environments, state.projectPath), ) - .then(environments => { + .then((environments) => { if (!environments) { createFlash( s__('Metrics|There was an error fetching the environments data, please try again'), @@ -297,7 +297,7 @@ export const fetchEnvironmentsData = ({ state, dispatch }) => { dispatch('receiveEnvironmentsDataSuccess', environments); }) - .catch(err => { + .catch((err) => { Sentry.captureException(err); dispatch('receiveEnvironmentsDataFailure'); createFlash(s__('Metrics|There was an error getting environments information.')); @@ -326,16 +326,18 @@ export const fetchAnnotations = ({ state, dispatch, getters }) => { startingFrom: start, }, }) - .then(resp => resp.data?.project?.environments?.nodes?.[0].metricsDashboard?.annotations.nodes) + .then( + (resp) => resp.data?.project?.environments?.nodes?.[0].metricsDashboard?.annotations.nodes, + ) .then(parseAnnotationsResponse) - .then(annotations => { + .then((annotations) => { if (!annotations) { createFlash(s__('Metrics|There was an error fetching annotations. Please try again.')); } dispatch('receiveAnnotationsSuccess', annotations); }) - .catch(err => { + .catch((err) => { Sentry.captureException(err); dispatch('receiveAnnotationsFailure'); createFlash(s__('Metrics|There was an error getting annotations information.')); @@ -363,7 +365,7 @@ export const fetchDashboardValidationWarnings = ({ state, dispatch, getters }) = dashboardPath, }, }) - .then(resp => resp.data?.project?.environments?.nodes?.[0]?.metricsDashboard) + .then((resp) => resp.data?.project?.environments?.nodes?.[0]?.metricsDashboard) .then(({ schemaValidationWarnings } = {}) => { const hasWarnings = schemaValidationWarnings && schemaValidationWarnings.length !== 0; /** @@ -372,7 +374,7 @@ export const fetchDashboardValidationWarnings = ({ state, dispatch, getters }) = */ dispatch('receiveDashboardValidationWarningsSuccess', hasWarnings || false); }) - .catch(err => { + .catch((err) => { Sentry.captureException(err); dispatch('receiveDashboardValidationWarningsFailure'); createFlash( @@ -437,9 +439,9 @@ export const duplicateSystemDashboard = ({ state }, payload) => { return axios .post(state.dashboardsEndpoint, params) - .then(response => response.data) - .then(data => data.dashboard) - .catch(error => { + .then((response) => response.data) + .then((data) => data.dashboard) + .catch((error) => { Sentry.captureException(error); const { response } = error; @@ -466,7 +468,7 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery const { start_time, end_time } = defaultQueryParams; const optionsRequests = []; - state.variables.forEach(variable => { + state.variables.forEach((variable) => { if (variable.type === VARIABLE_TYPES.metric_label_values) { const { prometheusEndpointPath, label } = variable.options; @@ -474,7 +476,7 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery start_time, end_time, }) - .then(data => { + .then((data) => { commit(types.UPDATE_VARIABLE_METRIC_LABEL_VALUES, { variable, label, data }); }) .catch(() => { @@ -512,7 +514,7 @@ export const fetchPanelPreview = ({ state, commit, dispatch }, panelPreviewYml) dispatch('fetchPanelPreviewMetrics'); }) - .catch(error => { + .catch((error) => { commit(types.RECEIVE_PANEL_PREVIEW_FAILURE, extractErrorMessage(error)); }); }; @@ -535,10 +537,10 @@ export const fetchPanelPreviewMetrics = ({ state, commit }) => { return getPrometheusQueryData(metric.prometheusEndpointPath, params, { cancelToken: cancelTokenSource.token, }) - .then(data => { + .then((data) => { commit(types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS, { index, data }); }) - .catch(error => { + .catch((error) => { Sentry.captureException(error); commit(types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE, { index, error }); diff --git a/app/assets/javascripts/monitoring/stores/embed_group/getters.js b/app/assets/javascripts/monitoring/stores/embed_group/getters.js index 47db787dea5..8eddd830c58 100644 --- a/app/assets/javascripts/monitoring/stores/embed_group/getters.js +++ b/app/assets/javascripts/monitoring/stores/embed_group/getters.js @@ -1,2 +1,2 @@ export const metricsWithData = (state, getters, rootState, rootGetters) => - state.modules.map(module => rootGetters[`${module}/metricsWithData`]().length); + state.modules.map((module) => rootGetters[`${module}/metricsWithData`]().length); diff --git a/app/assets/javascripts/monitoring/stores/getters.js b/app/assets/javascripts/monitoring/stores/getters.js index 8ed83cf02fe..d6a04006264 100644 --- a/app/assets/javascripts/monitoring/stores/getters.js +++ b/app/assets/javascripts/monitoring/stores/getters.js @@ -5,8 +5,10 @@ import { normalizeCustomDashboardPath, } from './utils'; -const metricsIdsInPanel = panel => - panel.metrics.filter(metric => metric.metricId && metric.result).map(metric => metric.metricId); +const metricsIdsInPanel = (panel) => + panel.metrics + .filter((metric) => metric.metricId && metric.result) + .map((metric) => metric.metricId); /** * Returns a reference to the currently selected dashboard @@ -17,8 +19,8 @@ const metricsIdsInPanel = panel => export const selectedDashboard = (state, getters) => { const { allDashboards } = state; return ( - allDashboards.find(d => d.path === getters.fullDashboardPath) || - allDashboards.find(d => d.default) || + allDashboards.find((d) => d.path === getters.fullDashboardPath) || + allDashboards.find((d) => d.default) || null ); }; @@ -32,15 +34,15 @@ export const selectedDashboard = (state, getters) => { * @returns {Function} A function that returns an array of * states in all the metric in the dashboard or group. */ -export const getMetricStates = state => groupKey => { +export const getMetricStates = (state) => (groupKey) => { let groups = state.dashboard.panelGroups; if (groupKey) { - groups = groups.filter(group => group.key === groupKey); + groups = groups.filter((group) => group.key === groupKey); } const metricStates = groups.reduce((acc, group) => { - group.panels.forEach(panel => { - panel.metrics.forEach(metric => { + group.panels.forEach((panel) => { + panel.metrics.forEach((metric) => { if (metric.state) { acc.push(metric.state); } @@ -64,15 +66,15 @@ export const getMetricStates = state => groupKey => { * metrics in the dashboard that contain results, optionally * filtered by group key. */ -export const metricsWithData = state => groupKey => { +export const metricsWithData = (state) => (groupKey) => { let groups = state.dashboard.panelGroups; if (groupKey) { - groups = groups.filter(group => group.key === groupKey); + groups = groups.filter((group) => group.key === groupKey); } const res = []; - groups.forEach(group => { - group.panels.forEach(panel => { + groups.forEach((group) => { + group.panels.forEach((panel) => { res.push(...metricsIdsInPanel(panel)); }); }); @@ -89,7 +91,7 @@ export const metricsWithData = state => groupKey => { * https://gitlab.com/gitlab-org/gitlab/-/issues/28241 * https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27447 */ -export const metricsSavedToDb = state => { +export const metricsSavedToDb = (state) => { const metricIds = []; state.dashboard.panelGroups.forEach(({ panels }) => { panels.forEach(({ metrics }) => { @@ -111,8 +113,8 @@ export const metricsSavedToDb = state => { * @param {Object} state * @returns {Array} List of environments */ -export const filteredEnvironments = state => - state.environments.filter(env => +export const filteredEnvironments = (state) => + state.environments.filter((env) => env.name.toLowerCase().includes((state.environmentsSearchTerm || '').trim().toLowerCase()), ); @@ -125,7 +127,7 @@ export const filteredEnvironments = state => * @param {Object} state * @returns {Array} modified array of links */ -export const linksWithMetadata = state => { +export const linksWithMetadata = (state) => { const metadata = { timeRange: state.timeRange, }; @@ -152,7 +154,7 @@ export const linksWithMetadata = state => { * in the format of {variables[key1]=value1, variables[key2]=value2} */ -export const getCustomVariablesParams = state => +export const getCustomVariablesParams = (state) => state.variables.reduce((acc, variable) => { const { name, value } = variable; if (value !== null) { @@ -168,5 +170,5 @@ export const getCustomVariablesParams = state => * @param {Object} state * @returns {String} full dashboard path */ -export const fullDashboardPath = state => +export const fullDashboardPath = (state) => normalizeCustomDashboardPath(state.currentDashboard, state.customDashboardBasePath); diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js index 09a5861b475..5c5a7d03b97 100644 --- a/app/assets/javascripts/monitoring/stores/mutations.js +++ b/app/assets/javascripts/monitoring/stores/mutations.js @@ -15,9 +15,9 @@ import { optionsFromSeriesData } from './variable_mapping'; */ const findMetricInDashboard = (metricId, dashboard) => { let res = null; - dashboard.panelGroups.forEach(group => { - group.panels.forEach(panel => { - panel.metrics.forEach(metric => { + dashboard.panelGroups.forEach((group) => { + group.panels.forEach((panel) => { + panel.metrics.forEach((metric) => { if (metric.metricId === metricId) { res = metric; } @@ -31,7 +31,7 @@ const findMetricInDashboard = (metricId, dashboard) => { * Maps a backened error state to a `metricStates` constant * @param {Object} error - Error from backend response */ -const emptyStateFromError = error => { +const emptyStateFromError = (error) => { if (!error) { return metricStates.UNKNOWN_ERROR; } @@ -53,7 +53,7 @@ const emptyStateFromError = error => { return metricStates.UNKNOWN_ERROR; }; -export const metricStateFromData = data => { +export const metricStateFromData = (data) => { if (data?.result?.length) { const result = normalizeQueryResponseData(data); return { state: metricStates.OK, result: Object.freeze(result) }; @@ -93,7 +93,7 @@ export default { state.isUpdatingStarredValue = true; }, [types.RECEIVE_DASHBOARD_STARRING_SUCCESS](state, { selectedDashboard, newStarredValue }) { - const index = state.allDashboards.findIndex(d => d === selectedDashboard); + const index = state.allDashboards.findIndex((d) => d === selectedDashboard); state.isUpdatingStarredValue = false; @@ -196,7 +196,7 @@ export default { state.showErrorBanner = enabled; }, [types.SET_PANEL_GROUP_METRICS](state, payload) { - const panelGroup = state.dashboard.panelGroups.find(pg => payload.key === pg.key); + const panelGroup = state.dashboard.panelGroups.find((pg) => payload.key === pg.key); panelGroup.panels = payload.panels; }, [types.SET_ENVIRONMENTS_FILTER](state, searchTerm) { @@ -207,7 +207,7 @@ export default { state.expandedPanel.panel = panel; }, [types.UPDATE_VARIABLE_VALUE](state, { name, value }) { - const variable = state.variables.find(v => v.name === name); + const variable = state.variables.find((v) => v.name === name); if (variable) { Object.assign(variable, { value, diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js index df7f22e622f..36e5a135d59 100644 --- a/app/assets/javascripts/monitoring/stores/utils.js +++ b/app/assets/javascripts/monitoring/stores/utils.js @@ -40,7 +40,7 @@ export const uniqMetricsId = ({ metric_id, id }) => `${metric_id || NOT_IN_DB_PR * @param {String} str String with leading slash * @returns {String} */ -export const removeLeadingSlash = str => (str || '').replace(/^\/+/, ''); +export const removeLeadingSlash = (str) => (str || '').replace(/^\/+/, ''); /** * GraphQL environments API returns only id and name. @@ -52,7 +52,7 @@ export const removeLeadingSlash = str => (str || '').replace(/^\/+/, ''); * @returns {Array} */ export const parseEnvironmentsResponse = (response = [], projectPath) => - (response || []).map(env => { + (response || []).map((env) => { const id = getIdFromGraphQLId(env.id); return { ...env, @@ -75,11 +75,11 @@ export const parseEnvironmentsResponse = (response = [], projectPath) => * @param {Array} response annotations response * @returns {Array} parsed responses */ -export const parseAnnotationsResponse = response => { +export const parseAnnotationsResponse = (response) => { if (!response) { return []; } - return response.map(annotation => ({ + return response.map((annotation) => ({ ...annotation, startingAt: new Date(annotation.startingAt), endingAt: annotation.endingAt ? new Date(annotation.endingAt) : null, @@ -99,7 +99,7 @@ export const parseAnnotationsResponse = response => { * @param {Array} metrics - Array of prometheus metrics * @returns {Object} */ -const mapToMetricsViewModel = metrics => +const mapToMetricsViewModel = (metrics) => metrics.map(({ label, id, metric_id, query_range, prometheus_endpoint_path, ...metric }) => ({ label, queryRange: query_range, @@ -230,7 +230,7 @@ const mapToPanelGroupViewModel = ({ group = '', panels = [] }, i) => { * @param {Object} timeRange * @returns {Object} */ -export const convertToGrafanaTimeRange = timeRange => { +export const convertToGrafanaTimeRange = (timeRange) => { const timeRangeType = getRangeType(timeRange); if (timeRangeType === DATETIME_RANGE_TYPES.fixed) { return { @@ -272,7 +272,7 @@ export const convertTimeRanges = (timeRange, type) => { * @param {Object} metadata * @returns {Function} */ -export const addDashboardMetaDataToLink = metadata => link => { +export const addDashboardMetaDataToLink = (metadata) => (link) => { let modifiedLink = { ...link }; if (metadata.timeRange) { modifiedLink = { @@ -307,7 +307,7 @@ export const mapToDashboardViewModel = ({ // Prometheus Results Parsing -const dateTimeFromUnixTime = unixTime => new Date(unixTime * 1000).toISOString(); +const dateTimeFromUnixTime = (unixTime) => new Date(unixTime * 1000).toISOString(); const mapScalarValue = ([unixTime, value]) => [dateTimeFromUnixTime(unixTime), Number(value)]; @@ -324,7 +324,7 @@ const mapStringValue = ([unixTime, value]) => [dateTimeFromUnixTime(unixTime), v * @param {array} result * @returns {array} */ -const normalizeScalarResult = result => [ +const normalizeScalarResult = (result) => [ { metric: {}, value: mapScalarValue(result), @@ -344,7 +344,7 @@ const normalizeScalarResult = result => [ * @param {array} result * @returns {array} */ -const normalizeStringResult = result => [ +const normalizeStringResult = (result) => [ { metric: {}, value: mapStringValue(result), @@ -379,7 +379,7 @@ const normalizeStringResult = result => [ * @param {array} result * @returns {array} */ -const normalizeVectorResult = result => +const normalizeVectorResult = (result) => result.map(({ metric, value }) => { const scalar = mapScalarValue(value); // Add a single element to `values`, to support matrix @@ -407,7 +407,7 @@ const normalizeVectorResult = result => * @param {array} result * @returns {object} Normalized result. */ -const normalizeResultMatrix = result => +const normalizeResultMatrix = (result) => result.map(({ metric, values }) => { const mappedValues = values.map(mapScalarValue); return { @@ -440,7 +440,7 @@ const normalizeResultMatrix = result => * ] * */ -export const normalizeQueryResponseData = data => { +export const normalizeQueryResponseData = (data) => { const { resultType, result } = data; if (resultType === 'vector') { return normalizeVectorResult(result); @@ -466,7 +466,7 @@ export const normalizeQueryResponseData = data => { * @param {String} name Variable key that needs to be prefixed * @returns {String} */ -export const addPrefixToCustomVariableParams = name => `variables[${name}]`; +export const addPrefixToCustomVariableParams = (name) => `variables[${name}]`; /** * Normalize custom dashboard paths. This method helps support diff --git a/app/assets/javascripts/monitoring/stores/variable_mapping.js b/app/assets/javascripts/monitoring/stores/variable_mapping.js index 4ae5cf04ff9..c9e0e383582 100644 --- a/app/assets/javascripts/monitoring/stores/variable_mapping.js +++ b/app/assets/javascripts/monitoring/stores/variable_mapping.js @@ -21,7 +21,7 @@ import { VARIABLE_TYPES } from '../constants'; * @param {String|Object} simpleTextVar * @returns {Object} */ -const textSimpleVariableParser = simpleTextVar => ({ +const textSimpleVariableParser = (simpleTextVar) => ({ type: VARIABLE_TYPES.text, label: null, value: simpleTextVar, @@ -34,7 +34,7 @@ const textSimpleVariableParser = simpleTextVar => ({ * @param {Object} advTextVar * @returns {Object} */ -const textAdvancedVariableParser = advTextVar => ({ +const textAdvancedVariableParser = (advTextVar) => ({ type: VARIABLE_TYPES.text, label: advTextVar.label, value: advTextVar.options.default_value, @@ -62,9 +62,9 @@ const normalizeVariableValues = ({ default: defaultOpt = false, text, value = nu * @param {Object} advVariable advanced custom variable * @returns {Object} */ -const customAdvancedVariableParser = advVariable => { +const customAdvancedVariableParser = (advVariable) => { const values = (advVariable?.options?.values ?? []).map(normalizeVariableValues); - const defaultValue = values.find(opt => opt.default === true) || values[0]; + const defaultValue = values.find((opt) => opt.default === true) || values[0]; return { type: VARIABLE_TYPES.custom, label: advVariable.label, @@ -82,7 +82,7 @@ const customAdvancedVariableParser = advVariable => { * @param {String} opt option from simple custom variable * @returns {Object} */ -export const parseSimpleCustomValues = opt => ({ text: opt, value: opt }); +export const parseSimpleCustomValues = (opt) => ({ text: opt, value: opt }); /** * Custom simple variables are rendered as dropdown elements in the dashboard @@ -96,7 +96,7 @@ export const parseSimpleCustomValues = opt => ({ text: opt, value: opt }); * @param {Array} customVariable array of options * @returns {Object} */ -const customSimpleVariableParser = simpleVar => { +const customSimpleVariableParser = (simpleVar) => { const values = (simpleVar || []).map(parseSimpleCustomValues); return { type: VARIABLE_TYPES.custom, @@ -126,7 +126,7 @@ const metricLabelValuesVariableParser = ({ label, options = {} }) => ({ * @param {Array|Object} customVar Array if simple, object if advanced * @returns {Boolean} true if simple, false if advanced */ -const isSimpleCustomVariable = customVar => Array.isArray(customVar); +const isSimpleCustomVariable = (customVar) => Array.isArray(customVar); /** * This method returns a parser based on the type of the variable. @@ -137,7 +137,7 @@ const isSimpleCustomVariable = customVar => Array.isArray(customVar); * @param {Array|Object} variable * @return {Function} parser method */ -const getVariableParser = variable => { +const getVariableParser = (variable) => { if (isString(variable)) { return textSimpleVariableParser; } else if (isSimpleCustomVariable(variable)) { @@ -200,7 +200,7 @@ export const parseTemplatingVariables = (ymlVariables = {}) => */ export const mergeURLVariables = (parsedYmlVariables = []) => { const varsFromURL = templatingVariablesFromUrl(); - parsedYmlVariables.forEach(variable => { + parsedYmlVariables.forEach((variable) => { const { name } = variable; if (Object.prototype.hasOwnProperty.call(varsFromURL, name)) { Object.assign(variable, { value: varsFromURL[name] }); diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js index a4c5a881fae..01cae7127e5 100644 --- a/app/assets/javascripts/monitoring/utils.js +++ b/app/assets/javascripts/monitoring/utils.js @@ -81,15 +81,15 @@ export const graphDataValidatorForValues = (isValues, graphData) => { const responseValueKeyName = isValues ? 'value' : 'values'; return ( Array.isArray(graphData.metrics) && - graphData.metrics.filter(query => { + graphData.metrics.filter((query) => { if (Array.isArray(query.result)) { return ( - query.result.filter(res => Array.isArray(res[responseValueKeyName])).length === + query.result.filter((res) => Array.isArray(res[responseValueKeyName])).length === query.result.length ); } return false; - }).length === graphData.metrics.filter(query => query.result).length + }).length === graphData.metrics.filter((query) => query.result).length ); }; @@ -106,7 +106,7 @@ const isClusterHealthBoard = () => (document.body.dataset.page || '').includes(' * @param {String} chart link that will be sent as a property for the event * @return {Object} config object for event tracking */ -export const generateLinkToChartOptions = chartLink => { +export const generateLinkToChartOptions = (chartLink) => { const isCLusterHealthBoard = isClusterHealthBoard(); const category = isCLusterHealthBoard @@ -124,7 +124,7 @@ export const generateLinkToChartOptions = chartLink => { * @param {String} chart title that will be sent as a property for the event * @return {Object} config object for event tracking */ -export const downloadCSVOptions = title => { +export const downloadCSVOptions = (title) => { const isCLusterHealthBoard = isClusterHealthBoard(); const category = isCLusterHealthBoard @@ -157,7 +157,7 @@ export const getAddMetricTrackingOptions = () => ({ * @param {Object} graphData the graph data response from a prometheus request * @returns {boolean} true if the data is valid */ -export const graphDataValidatorForAnomalyValues = graphData => { +export const graphDataValidatorForAnomalyValues = (graphData) => { const anomalySeriesCount = 3; // metric, upper, lower return ( graphData.metrics && @@ -186,7 +186,7 @@ export const timeRangeFromUrl = (search = window.location.search) => { * @param {String} label label for the template variable * @returns {String} */ -export const addPrefixToLabel = label => `${VARIABLE_PREFIX}${label}`; +export const addPrefixToLabel = (label) => `${VARIABLE_PREFIX}${label}`; /** * Before the templating variables are passed to the backend the @@ -197,7 +197,7 @@ export const addPrefixToLabel = label => `${VARIABLE_PREFIX}${label}`; * @param {String} label label to remove prefix from * @returns {String} */ -export const removePrefixFromLabel = label => +export const removePrefixFromLabel = (label) => (label || '').replace(new RegExp(`^${VARIABLE_PREFIX}`), ''); /** @@ -210,7 +210,7 @@ export const removePrefixFromLabel = label => * @param {Object} variables * @returns {Object} */ -export const convertVariablesForURL = variables => +export const convertVariablesForURL = (variables) => variables.reduce((acc, { name, value }) => { if (value !== null) { acc[addPrefixToLabel(name)] = value; @@ -241,7 +241,7 @@ export const templatingVariablesFromUrl = (search = window.location.search) => { * * @param {Object} variables user defined variables */ -export const setCustomVariablesFromUrl = variables => { +export const setCustomVariablesFromUrl = (variables) => { // prep the variables to append to URL const parsedVariables = convertVariablesForURL(variables); // update the URL @@ -353,7 +353,7 @@ export const panelToUrl = ( * @param {Array} values data points * @returns {Number} */ -const metricValueMapper = values => values[0]?.[1]; +const metricValueMapper = (values) => values[0]?.[1]; /** * Get the metric name from metric object @@ -364,7 +364,7 @@ const metricValueMapper = values => values[0]?.[1]; * @param {Object} metric metric object * @returns {String} */ -const metricNameMapper = metric => Object.values(metric)?.[0]; +const metricNameMapper = (metric) => Object.values(metric)?.[0]; /** * Parse metric object to extract metric value and name in diff --git a/app/assets/javascripts/monitoring/validators.js b/app/assets/javascripts/monitoring/validators.js index c6b323f6360..05a9d8b9db5 100644 --- a/app/assets/javascripts/monitoring/validators.js +++ b/app/assets/javascripts/monitoring/validators.js @@ -1,6 +1,6 @@ import { isSafeURL } from '~/lib/utils/url_utility'; -const isRunbookUrlValid = runbookUrl => { +const isRunbookUrlValid = (runbookUrl) => { if (!runbookUrl) { return true; } @@ -21,7 +21,7 @@ const isRunbookUrlValid = runbookUrl => { // } // } export function alertsValidator(value) { - return Object.keys(value).every(key => { + return Object.keys(value).every((key) => { const alert = value[key]; return ( alert.alert_path && @@ -49,7 +49,7 @@ export function alertsValidator(value) { // ] export function queriesValidator(value) { return value.every( - query => + (query) => query.metricId && typeof query.metricId === 'string' && typeof query.label === 'string', ); } diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js index 2be7cc951fc..ab88a610469 100644 --- a/app/assets/javascripts/mr_notes/init_notes.js +++ b/app/assets/javascripts/mr_notes/init_notes.js @@ -33,7 +33,7 @@ export default () => { computed: { ...mapGetters(['discussionTabCounter']), ...mapState({ - activeTab: state => state.page.activeTab, + activeTab: (state) => state.page.activeTab, }), isShowTabActive() { return this.activeTab === 'show'; diff --git a/app/assets/javascripts/mr_notes/stores/getters.js b/app/assets/javascripts/mr_notes/stores/getters.js index 245443d7ecf..556254486db 100644 --- a/app/assets/javascripts/mr_notes/stores/getters.js +++ b/app/assets/javascripts/mr_notes/stores/getters.js @@ -1,7 +1,7 @@ // Note: this getter is important because // `noteableData` is namespaced under `notes` for `~/mr_notes/stores` // while `noteableData` is directly available as `state.noteableData` for `~/notes/stores` -export const getNoteableData = state => state.notes.noteableData; +export const getNoteableData = (state) => state.notes.noteableData; export default { isLoggedIn(state, getters) { diff --git a/app/assets/javascripts/mr_popover/components/mr_popover.vue b/app/assets/javascripts/mr_popover/components/mr_popover.vue index bf810978648..2058f0c9b76 100644 --- a/app/assets/javascripts/mr_popover/components/mr_popover.vue +++ b/app/assets/javascripts/mr_popover/components/mr_popover.vue @@ -72,7 +72,7 @@ export default { apollo: { mergeRequest: { query, - update: data => data.project.mergeRequest, + update: (data) => data.project.mergeRequest, variables() { const { projectPath, mergeRequestIID } = this; diff --git a/app/assets/javascripts/mr_popover/index.js b/app/assets/javascripts/mr_popover/index.js index 18c0e201300..03ddfd13d50 100644 --- a/app/assets/javascripts/mr_popover/index.js +++ b/app/assets/javascripts/mr_popover/index.js @@ -42,7 +42,7 @@ const handleMRPopoverMount = ({ apolloProvider, projectPath, mrTitle, iid }) => }, 200); // 200ms delay so not every mouseover triggers Popover + API Call }; -export default elements => { +export default (elements) => { const mrLinks = elements || [...document.querySelectorAll('.gfm-merge_request')]; if (mrLinks.length > 0) { Vue.use(VueApollo); @@ -52,7 +52,7 @@ export default elements => { }); const listenerAddedAttr = 'data-mr-listener-added'; - mrLinks.forEach(el => { + mrLinks.forEach((el) => { const { projectPath, mrTitle, iid } = el.dataset; if (!el.getAttribute(listenerAddedAttr) && projectPath && mrTitle && iid) { diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js index 8e123c14814..e668b492ebe 100644 --- a/app/assets/javascripts/namespace_select.js +++ b/app/assets/javascripts/namespace_select.js @@ -25,7 +25,7 @@ export default class NamespaceSelect { return `${selected.kind}: ${selected.full_path}`; }, data(term, dataCallback) { - return Api.namespaces(term, namespaces => { + return Api.namespaces(term, (namespaces) => { if (isFilter) { const anyNamespace = { text: __('Any namespace'), diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index 3ea597a08d3..3574fc47088 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -66,12 +66,12 @@ export default class BranchGraph { collectParents() { const ref = this.commits; const results = []; - ref.forEach(c => { + ref.forEach((c) => { this.mtime = Math.max(this.mtime, c.time); this.mspace = Math.max(this.mspace, c.space); const ref1 = c.parents; const results1 = []; - ref1.forEach(p => { + ref1.forEach((p) => { this.parents[p[0]] = true; results1.push((this.mspace = Math.max(this.mspace, p[1]))); }); @@ -243,11 +243,11 @@ export default class BranchGraph { }) .click(() => window.open(options.commit_url.replace('%s', commit.id), '_blank')) .hover( - function() { + function () { this.tooltip = r.commitTooltip(x + 5, y, commit); return top.push(this.tooltip.insertBefore(this)); }, - function() { + function () { return this.tooltip && this.tooltip.remove() && delete this.tooltip; }, ); diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js index 9d064894433..d93db7399e6 100644 --- a/app/assets/javascripts/new_branch_form.js +++ b/app/assets/javascripts/new_branch_form.js @@ -53,14 +53,14 @@ export default class NewBranchForm { const { indexOf } = []; this.branchNameError.empty(); - const unique = function(values, value) { + const unique = function (values, value) { if (indexOf.call(values, value) === -1) { values.push(value); } return values; }; - const formatter = function(values, restriction) { - const formatted = values.map(value => { + const formatter = function (values, restriction) { + const formatted = values.map((value) => { switch (false) { case !/\s/.test(value): return 'spaces'; diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue index c04f2a2d465..d1d5ae5265a 100644 --- a/app/assets/javascripts/notebook/cells/markdown.vue +++ b/app/assets/javascripts/notebook/cells/markdown.vue @@ -77,11 +77,11 @@ function renderKatex(t) { } return [text, numInline > 0]; } -renderer.paragraph = t => { +renderer.paragraph = (t) => { const [text, inline] = renderKatex(t); return `<p class="${inline ? 'inline-katex' : ''}">${text}</p>`; }; -renderer.listitem = t => { +renderer.listitem = (t) => { const [text, inline] = renderKatex(t); return `<li class="${inline ? 'inline-katex' : ''}">${text}</li>`; }; diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue index 113d8cfc435..5f7ef4a4377 100644 --- a/app/assets/javascripts/notebook/cells/output/index.vue +++ b/app/assets/javascripts/notebook/cells/output/index.vue @@ -2,6 +2,7 @@ import CodeOutput from '../code/index.vue'; import HtmlOutput from './html.vue'; import ImageOutput from './image.vue'; +import LatexOutput from './latex.vue'; export default { props: { @@ -35,6 +36,8 @@ export default { return 'image/jpeg'; } else if (output.data['text/html']) { return 'text/html'; + } else if (output.data['text/latex']) { + return 'text/latex'; } else if (output.data['image/svg+xml']) { return 'image/svg+xml'; } @@ -59,6 +62,8 @@ export default { return ImageOutput; } else if (output.data['text/html']) { return HtmlOutput; + } else if (output.data['text/latex']) { + return LatexOutput; } else if (output.data['image/svg+xml']) { return HtmlOutput; } diff --git a/app/assets/javascripts/notebook/cells/output/latex.vue b/app/assets/javascripts/notebook/cells/output/latex.vue new file mode 100644 index 00000000000..db9e61dce82 --- /dev/null +++ b/app/assets/javascripts/notebook/cells/output/latex.vue @@ -0,0 +1,45 @@ +<script> +import 'mathjax/es5/tex-svg'; +import Prompt from '../prompt.vue'; + +export default { + name: 'LatexOutput', + components: { + Prompt, + }, + props: { + count: { + type: Number, + required: true, + }, + rawCode: { + type: String, + required: true, + }, + index: { + type: Number, + required: true, + }, + }, + computed: { + code() { + // MathJax will not parse out the inline delimeters "$$" correctly + // so we remove them from the raw code itself + const parsedCode = this.rawCode.replace(/\$\$/g, ''); + const svg = window.MathJax.tex2svg(parsedCode); + + // NOTE: This is used with `v-html` and not `v-safe-html` due to an + // issue with dompurify stripping out xlink attributes from use tags + return svg.outerHTML; + }, + }, +}; +</script> + +<template> + <div class="output"> + <prompt type="Out" :count="count" :show-output="index === 0" /> + <!-- eslint-disable --> + <div ref="maths" v-html="code"></div> + </div> +</template> diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 9a887021e5d..857e5a34db6 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -108,10 +108,7 @@ export default class Notes { // We are in the Merge Requests page so we need another edit form for Changes tab if (getPagePath(1) === 'merge_requests') { - $('.note-edit-form') - .clone() - .addClass('mr-note-edit-form') - .insertAfter('.note-edit-form'); + $('.note-edit-form').clone().addClass('mr-note-edit-form').insertAfter('.note-edit-form'); } const hash = getLocationHash(); @@ -348,7 +345,7 @@ export default class Notes { votesBlock = $('.js-awards-block').eq(0); loadAwardsHandler() - .then(awardsHandler => { + .then((awardsHandler) => { awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award); awardsHandler.scrollToAwards(); }) @@ -427,12 +424,7 @@ export default class Notes { } else if (Notes.isUpdatedNote(noteEntity, $note)) { // The server can send the same update multiple times so we need to make sure to only update once per actual update. const isEditing = $note.hasClass('is-editing'); - const initialContent = normalizeNewlines( - $note - .find('.original-note-content') - .text() - .trim(), - ); + const initialContent = normalizeNewlines($note.find('.original-note-content').text().trim()); const $textarea = $note.find('.js-note-text'); const currentContent = $textarea.val(); // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way @@ -541,14 +533,8 @@ export default class Notes { form.find('.js-errors').remove(); // reset text and preview form.find('.js-md-write-button').click(); - form - .find('.js-note-text') - .val('') - .trigger('input'); - form - .find('.js-note-text') - .data('autosave') - .reset(); + form.find('.js-note-text').val('').trigger('input'); + form.find('.js-note-text').data('autosave').reset(); const event = document.createEvent('Event'); event.initEvent('autosize:update', true, false); @@ -690,10 +676,7 @@ export default class Notes { } checkContentToAllowEditing($el) { - const initialContent = $el - .find('.original-note-content') - .text() - .trim(); + const initialContent = $el.find('.original-note-content').text().trim(); const currentContent = $el.find('.js-note-text').val(); let isAllowed = true; @@ -877,10 +860,7 @@ export default class Notes { const form = this.cleanForm(this.formClone.clone()); const replyLink = $(target).closest('.js-discussion-reply-button'); // insert the form after the button - replyLink - .closest('.discussion-reply-holder') - .hide() - .after(form); + replyLink.closest('.discussion-reply-holder').hide().after(form); // show the form return this.setupDiscussionNoteForm(replyLink, form); } @@ -1069,10 +1049,7 @@ export default class Notes { const row = form.closest('tr'); const glForm = form.data('glForm'); glForm.destroy(); - form - .find('.js-note-text') - .data('autosave') - .reset(); + form.find('.js-note-text').data('autosave').reset(); // show the reply button (will only work for replies) form.prev('.discussion-reply-holder').show(); if (row.is('.js-temp-notes-holder')) { @@ -1181,16 +1158,10 @@ export default class Notes { this.glForm = new GLForm($editForm.find('form'), this.enableGFM); - $editForm - .find('form') - .attr('action', `${postUrl}?html=true`) - .attr('data-remote', 'true'); + $editForm.find('form').attr('action', `${postUrl}?html=true`).attr('data-remote', 'true'); $editForm.find('.js-form-target-id').val(targetId); $editForm.find('.js-form-target-type').val(targetType); - $editForm - .find('.js-note-text') - .focus() - .val(originalContent); + $editForm.find('.js-note-text').focus().val(originalContent); $editForm.find('.js-md-write-button').trigger('click'); $editForm.find('.referenced-users').hide(); } @@ -1306,9 +1277,7 @@ export default class Notes { * intrusive. */ collapseLongCommitList() { - const systemNotes = $('#notes-list') - .find('li.system-note') - .has('ul'); + const systemNotes = $('#notes-list').find('li.system-note').has('ul'); $.each(systemNotes, (index, systemNote) => { const $systemNote = $(systemNote); @@ -1365,11 +1334,7 @@ export default class Notes { // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way const sanitizedNoteEntityText = normalizeNewlines(noteEntity.note.trim()); const currentNoteText = normalizeNewlines( - $note - .find('.original-note-content') - .first() - .text() - .trim(), + $note.find('.original-note-content').first().text().trim(), ); return sanitizedNoteEntityText !== currentNoteText; } @@ -1431,7 +1396,7 @@ export default class Notes { let tempFormContent; // Identify executed quick actions from `formContent` - const executedCommands = availableQuickActions.filter(command => { + const executedCommands = availableQuickActions.filter((command) => { const commandRegex = new RegExp(`/${command.name}`); return commandRegex.test(formContent); }); @@ -1545,10 +1510,7 @@ export default class Notes { let $form = $submitBtn.parents('form'); const $closeBtn = $form.find('.js-note-target-close'); const isDiscussionNote = - $submitBtn - .parent() - .find('li.droplab-item-selected') - .attr('id') === 'discussion'; + $submitBtn.parent().find('li.droplab-item-selected').attr('id') === 'discussion'; const isMainForm = $form.hasClass('js-main-target-form'); const isDiscussionForm = $form.hasClass('js-discussion-note-form'); const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button'); @@ -1621,7 +1583,7 @@ export default class Notes { // Make request to submit comment on server return axios .post(`${formAction}?html=true`, formData) - .then(res => { + .then((res) => { const note = res.data; $submitBtn.prop('disabled', false); diff --git a/app/assets/javascripts/notes/components/comment_field_layout.vue b/app/assets/javascripts/notes/components/comment_field_layout.vue new file mode 100644 index 00000000000..aaf64702ffd --- /dev/null +++ b/app/assets/javascripts/notes/components/comment_field_layout.vue @@ -0,0 +1,69 @@ +<script> +import EmailParticipantsWarning from './email_participants_warning.vue'; +import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue'; + +const DEFAULT_NOTEABLE_TYPE = 'Issue'; + +export default { + components: { + EmailParticipantsWarning, + NoteableWarning, + }, + props: { + noteableData: { + type: Object, + required: true, + }, + noteableType: { + type: String, + required: false, + default: DEFAULT_NOTEABLE_TYPE, + }, + withAlertContainer: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + isLocked() { + return Boolean(this.noteableData.discussion_locked); + }, + isConfidential() { + return Boolean(this.noteableData.confidential); + }, + hasWarning() { + return this.isConfidential || this.isLocked; + }, + emailParticipants() { + return this.noteableData.issue_email_participants?.map(({ email }) => email) || []; + }, + }, +}; +</script> +<template> + <div + class="comment-warning-wrapper gl-border-solid gl-border-1 gl-rounded-base gl-border-gray-100" + > + <div + v-if="withAlertContainer" + class="error-alert" + data-testid="comment-field-alert-container" + ></div> + <noteable-warning + v-if="hasWarning" + class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-100 gl-rounded-base gl-rounded-bottom-left-none gl-rounded-bottom-right-none" + :is-locked="isLocked" + :is-confidential="isConfidential" + :noteable-type="noteableType" + :locked-noteable-docs-path="noteableData.locked_discussion_docs_path" + :confidential-noteable-docs-path="noteableData.confidential_issues_docs_path" + /> + <slot></slot> + <email-participants-warning + v-if="emailParticipants.length" + class="gl-border-t-1 gl-border-t-solid gl-border-t-gray-100 gl-rounded-base gl-rounded-top-left-none! gl-rounded-top-right-none!" + :emails="emailParticipants" + /> + </div> +</template> diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 0363173f912..111af977ec5 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -17,17 +17,17 @@ import { import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import * as constants from '../constants'; import eventHub from '../event_hub'; -import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue'; import markdownField from '~/vue_shared/components/markdown/field.vue'; import userAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import noteSignedOutWidget from './note_signed_out_widget.vue'; import discussionLockedWidget from './discussion_locked_widget.vue'; import issuableStateMixin from '../mixins/issuable_state'; +import CommentFieldLayout from './comment_field_layout.vue'; export default { name: 'CommentForm', components: { - NoteableWarning, noteSignedOutWidget, discussionLockedWidget, markdownField, @@ -35,8 +35,9 @@ export default { GlButton, TimelineEntryItem, GlIcon, + CommentFieldLayout, }, - mixins: [issuableStateMixin], + mixins: [glFeatureFlagsMixin(), issuableStateMixin], props: { noteableType: { type: String, @@ -286,6 +287,9 @@ export default { Autosize.update(this.$refs.textarea); }); }, + hasEmailParticipants() { + return this.getNoteableData.issue_email_participants?.length; + }, }, }; </script> @@ -308,46 +312,41 @@ export default { </div> <div class="timeline-content timeline-content-form"> <form ref="commentForm" class="new-note common-note-form gfm-form js-main-target-form"> - <div class="error-alert"></div> - - <noteable-warning - v-if="hasWarning(getNoteableData)" - :is-locked="isLocked(getNoteableData)" - :is-confidential="isConfidential(getNoteableData)" + <comment-field-layout + :with-alert-container="true" + :noteable-data="getNoteableData" :noteable-type="noteableType" - :locked-noteable-docs-path="lockedIssueDocsPath" - :confidential-noteable-docs-path="confidentialIssueDocsPath" - /> - - <markdown-field - ref="markdownField" - :is-submitting="isSubmitting" - :markdown-preview-path="markdownPreviewPath" - :markdown-docs-path="markdownDocsPath" - :quick-actions-docs-path="quickActionsDocsPath" - :add-spacing-classes="false" - :textarea-value="note" > - <textarea - id="note-body" - ref="textarea" - slot="textarea" - v-model="note" - dir="auto" - :disabled="isSubmitting" - name="note[note]" - class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area" - data-qa-selector="comment_field" - data-testid="comment-field" - data-supports-quick-actions="true" - :aria-label="__('Description')" - :placeholder="__('Write a comment or drag your files hereā¦')" - @keydown.up="editCurrentUserLastNote()" - @keydown.meta.enter="handleSave()" - @keydown.ctrl.enter="handleSave()" - ></textarea> - </markdown-field> - + <markdown-field + ref="markdownField" + :is-submitting="isSubmitting" + :markdown-preview-path="markdownPreviewPath" + :markdown-docs-path="markdownDocsPath" + :quick-actions-docs-path="quickActionsDocsPath" + :add-spacing-classes="false" + :textarea-value="note" + > + <template #textarea> + <textarea + id="note-body" + ref="textarea" + v-model="note" + dir="auto" + :disabled="isSubmitting" + name="note[note]" + class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area" + data-qa-selector="comment_field" + data-testid="comment-field" + :data-supports-quick-actions="!glFeatures.tributeAutocomplete" + :aria-label="__('Description')" + :placeholder="__('Write a comment or drag your files hereā¦')" + @keydown.up="editCurrentUserLastNote()" + @keydown.meta.enter="handleSave()" + @keydown.ctrl.enter="handleSave()" + ></textarea> + </template> + </markdown-field> + </comment-field-layout> <div class="note-form-actions"> <div class="btn-group gl-mr-3 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index 1580c94658a..b7355d4d927 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -31,7 +31,7 @@ export default { }, computed: { ...mapState({ - projectPath: state => state.diffs.projectPath, + projectPath: (state) => state.diffs.projectPath, }), diffMode() { return getDiffMode(this.discussion.diff_file); diff --git a/app/assets/javascripts/notes/components/discussion_actions.vue b/app/assets/javascripts/notes/components/discussion_actions.vue index 0272790a75d..da4134ab2c4 100644 --- a/app/assets/javascripts/notes/components/discussion_actions.vue +++ b/app/assets/javascripts/notes/components/discussion_actions.vue @@ -2,7 +2,6 @@ import ReplyPlaceholder from './discussion_reply_placeholder.vue'; import ResolveDiscussionButton from './discussion_resolve_button.vue'; import ResolveWithIssueButton from './discussion_resolve_with_issue_button.vue'; -import JumpToNextDiscussionButton from './discussion_jump_to_next_button.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { @@ -11,7 +10,6 @@ export default { ReplyPlaceholder, ResolveDiscussionButton, ResolveWithIssueButton, - JumpToNextDiscussionButton, }, mixins: [glFeatureFlagsMixin()], props: { @@ -38,14 +36,11 @@ export default { }, }, computed: { - hideJumpToNextUnresolvedInThreads() { - return this.glFeatures.hideJumpToNextUnresolvedInThreads; - }, resolvableNotes() { - return this.discussion.notes.filter(x => x.resolvable); + return this.discussion.notes.filter((x) => x.resolvable); }, userCanResolveDiscussion() { - return this.resolvableNotes.every(note => note.current_user?.can_resolve_discussion); + return this.resolvableNotes.every((note) => note.current_user?.can_resolve_discussion); }, }, }; @@ -74,15 +69,5 @@ export default { :url="resolveWithIssuePath" /> </div> - <div - v-if=" - !hideJumpToNextUnresolvedInThreads && - discussion.resolvable && - shouldShowJumpToNextDiscussion - " - class="btn-group discussion-actions ml-sm-2" - > - <jump-to-next-discussion-button :from-discussion-id="discussion.id" /> - </div> </div> </template> diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index 2427a3f98ad..0a72627834d 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -32,10 +32,10 @@ export default { return this.getNoteableData.create_issue_to_resolve_discussions_path; }, toggeableDiscussions() { - return this.discussions.filter(discussion => !discussion.individual_note); + return this.discussions.filter((discussion) => !discussion.individual_note); }, allExpanded() { - return this.toggeableDiscussions.every(discussion => discussion.expanded); + return this.toggeableDiscussions.every((discussion) => discussion.expanded); }, lineResolveClass() { return this.allResolved ? 'line-resolve-btn is-active' : 'line-resolve-text'; @@ -48,7 +48,7 @@ export default { ...mapActions(['setExpandDiscussions']), handleExpandDiscussions() { this.setExpandDiscussions({ - discussionIds: this.toggeableDiscussions.map(discussion => discussion.id), + discussionIds: this.toggeableDiscussions.map((discussion) => discussion.id), expanded: !this.allExpanded, }); }, diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue index 08c22f0b4c6..aa61aa9b3cb 100644 --- a/app/assets/javascripts/notes/components/discussion_filter.vue +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -42,7 +42,7 @@ export default { ...mapGetters(['getNotesDataByProp', 'timelineEnabled']), currentFilter() { if (!this.currentValue) return this.filters[0]; - return this.filters.find(filter => filter.value === this.currentValue); + return this.filters.find((filter) => filter.value === this.currentValue); }, }, created() { diff --git a/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue b/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue deleted file mode 100644 index f94d0060b41..00000000000 --- a/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue +++ /dev/null @@ -1,38 +0,0 @@ -<script> -import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; -import discussionNavigation from '../mixins/discussion_navigation'; - -export default { - name: 'JumpToNextDiscussionButton', - components: { - GlIcon, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - mixins: [discussionNavigation], - props: { - fromDiscussionId: { - type: String, - required: true, - }, - }, -}; -</script> - -<template> - <div class="btn-group" role="group"> - <button - ref="button" - v-gl-tooltip - class="btn btn-default discussion-next-btn" - :title="s__('MergeRequests|Jump to next unresolved thread')" - data-track-event="click_button" - data-track-label="mr_next_unresolved_thread" - data-track-property="click_next_unresolved_thread" - @click="jumpToNextRelativeDiscussion(fromDiscussionId)" - > - <gl-icon name="comment-next" /> - </button> - </div> -</template> diff --git a/app/assets/javascripts/notes/components/email_participants_warning.vue b/app/assets/javascripts/notes/components/email_participants_warning.vue new file mode 100644 index 00000000000..bb1ff58120a --- /dev/null +++ b/app/assets/javascripts/notes/components/email_participants_warning.vue @@ -0,0 +1,70 @@ +<script> +import { GlSprintf } from '@gitlab/ui'; +import { s__, sprintf } from '~/locale'; +import { toNounSeriesText } from '~/lib/utils/grammar'; + +export default { + components: { + GlSprintf, + }, + props: { + emails: { + type: Array, + required: true, + }, + numberOfLessParticipants: { + type: Number, + required: false, + default: 3, + }, + }, + data() { + return { + isShowingMoreParticipants: false, + }; + }, + computed: { + title() { + return this.moreParticipantsAvailable + ? toNounSeriesText(this.lessParticipants, { onlyCommas: true }) + : toNounSeriesText(this.emails); + }, + lessParticipants() { + return this.emails.slice(0, this.numberOfLessParticipants); + }, + moreLabel() { + return sprintf(s__('EmailParticipantsWarning|and %{moreCount} more'), { + moreCount: this.emails.length - this.numberOfLessParticipants, + }); + }, + moreParticipantsAvailable() { + return !this.isShowingMoreParticipants && this.emails.length > this.numberOfLessParticipants; + }, + message() { + return this.moreParticipantsAvailable + ? s__('EmailParticipantsWarning|%{emails}, %{andMore} will be notified of your comment.') + : s__('EmailParticipantsWarning|%{emails} will be notified of your comment.'); + }, + }, + methods: { + showMoreParticipants() { + this.isShowingMoreParticipants = true; + }, + }, +}; +</script> + +<template> + <div class="issuable-note-warning" data-testid="email-participants-warning"> + <gl-sprintf :message="message"> + <template #andMore> + <button type="button" class="btn-transparent btn-link" @click="showMoreParticipants"> + {{ moreLabel }} + </button> + </template> + <template #emails> + <span>{{ title }}</span> + </template> + </gl-sprintf> + </div> +</template> diff --git a/app/assets/javascripts/notes/components/multiline_comment_form.vue b/app/assets/javascripts/notes/components/multiline_comment_form.vue index bb13eb87af7..9fbf2c9265c 100644 --- a/app/assets/javascripts/notes/components/multiline_comment_form.vue +++ b/app/assets/javascripts/notes/components/multiline_comment_form.vue @@ -1,5 +1,5 @@ <script> -import { mapActions } from 'vuex'; +import { mapActions, mapState } from 'vuex'; import { GlFormSelect, GlSprintf } from '@gitlab/ui'; import { getSymbol, getLineClasses } from './multiline_comment_utils'; @@ -27,12 +27,13 @@ export default { }; }, computed: { + ...mapState({ selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition }), lineNumber() { return this.commentLineOptions[this.commentLineOptions.length - 1].text; }, }, created() { - const line = this.lineRange?.start || this.line; + const line = this.selectedCommentPosition?.start || this.lineRange?.start || this.line; this.commentLineStart = { line_code: line.line_code, @@ -40,6 +41,8 @@ export default { old_line: line.old_line, new_line: line.new_line, }; + + if (this.selectedCommentPosition) return; this.highlightSelection(); }, destroyed() { diff --git a/app/assets/javascripts/notes/components/multiline_comment_utils.js b/app/assets/javascripts/notes/components/multiline_comment_utils.js index 2451400e980..4991695b97e 100644 --- a/app/assets/javascripts/notes/components/multiline_comment_utils.js +++ b/app/assets/javascripts/notes/components/multiline_comment_utils.js @@ -48,11 +48,11 @@ export function getLineClasses(line) { export function commentLineOptions(diffLines, startingLine, lineCode, side = 'left') { const preferredSide = side === 'left' ? 'old_line' : 'new_line'; const fallbackSide = preferredSide === 'new_line' ? 'old_line' : 'new_line'; - const notMatchType = l => l.type !== 'match'; + const notMatchType = (l) => l.type !== 'match'; const linesCopy = [...diffLines]; // don't mutate the argument const startingLineCode = startingLine.line_code; - const currentIndex = linesCopy.findIndex(line => line.line_code === lineCode); + const currentIndex = linesCopy.findIndex((line) => line.line_code === lineCode); // We're limiting adding comments to only lines above the current line // to make rendering simpler. Future interations will use a more @@ -66,10 +66,10 @@ export function commentLineOptions(diffLines, startingLine, lineCode, side = 'le // If the selected line is "hidden" in an unchanged line block // or "above" the current group of lines add it to the array so // that the drop down is not defaulted to empty - const selectedIndex = lines.findIndex(line => line.line_code === startingLineCode); + const selectedIndex = lines.findIndex((line) => line.line_code === startingLineCode); if (selectedIndex < 0) lines.unshift(startingLine); - return lines.map(l => { + return lines.map((l) => { const { line_code, type, old_line, new_line } = l; return { value: { line_code, type, old_line, new_line }, @@ -103,7 +103,7 @@ export function getCommentedLines(selectedCommentPosition, diffLines) { }; } - const findLineCodeIndex = line => position => { + const findLineCodeIndex = (line) => (position) => { return [position.line_code, position.left?.line_code, position.right?.line_code].includes( line.line_code, ); diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index fc131f548b4..b85cfa83e09 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -206,14 +206,14 @@ export default { const { project_id, iid } = this.getNoteableData; if (this.isUserAssigned) { - assignees = assignees.filter(assignee => assignee.id !== this.author.id); + assignees = assignees.filter((assignee) => assignee.id !== this.author.id); } else { assignees.push({ id: this.author.id }); } if (this.targetType === 'issue') { Api.updateIssue(project_id, iid, { - assignee_ids: assignees.map(assignee => assignee.id), + assignee_ids: assignees.map((assignee) => assignee.id), }) .then(() => this.handleAssigneeUpdate(assignees)) .catch(() => flash(__('Something went wrong while updating assignees'))); diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index 65b89b94eaa..8855ceac3d5 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -52,8 +52,9 @@ export default { return this.getDiscussion(this.note.discussion_id); }, ...mapState({ - batchSuggestionsInfo: state => state.notes.batchSuggestionsInfo, + batchSuggestionsInfo: (state) => state.notes.batchSuggestionsInfo, }), + ...mapState('diffs', ['defaultSuggestionCommitMessage']), noteBody() { return this.note.note; }, @@ -98,12 +99,16 @@ export default { formCancelHandler(shouldConfirm, isDirty) { this.$emit('cancelForm', shouldConfirm, isDirty); }, - applySuggestion({ suggestionId, flashContainer, callback = () => {} }) { + applySuggestion({ suggestionId, flashContainer, callback = () => {}, message }) { const { discussion_id: discussionId, id: noteId } = this.note; - return this.submitSuggestion({ discussionId, noteId, suggestionId, flashContainer }).then( - callback, - ); + return this.submitSuggestion({ + discussionId, + noteId, + suggestionId, + flashContainer, + message, + }).then(callback); }, applySuggestionBatch({ flashContainer }) { return this.submitSuggestionBatch({ flashContainer }); @@ -130,6 +135,7 @@ export default { :note-html="note.note_html" :line-type="lineType" :help-page-path="helpPagePath" + :default-commit-message="defaultSuggestionCommitMessage" @apply="applySuggestion" @applyBatch="applySuggestionBatch" @addToBatch="addSuggestionToBatch" diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 84769bfc7c8..9acb837c27f 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -3,20 +3,21 @@ import { mapGetters, mapActions, mapState } from 'vuex'; import { mergeUrlParams } from '~/lib/utils/url_utility'; import eventHub from '../event_hub'; -import NoteableWarning from '../../vue_shared/components/notes/noteable_warning.vue'; -import markdownField from '../../vue_shared/components/markdown/field.vue'; +import markdownField from '~/vue_shared/components/markdown/field.vue'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import issuableStateMixin from '../mixins/issuable_state'; import resolvable from '../mixins/resolvable'; import { __, sprintf } from '~/locale'; import { getDraft, updateDraft } from '~/lib/utils/autosave'; +import CommentFieldLayout from './comment_field_layout.vue'; export default { name: 'NoteForm', components: { - NoteableWarning, markdownField, + CommentFieldLayout, }, - mixins: [issuableStateMixin, resolvable], + mixins: [glFeatureFlagsMixin(), issuableStateMixin, resolvable], props: { noteBody: { type: String, @@ -114,7 +115,7 @@ export default { 'getUserDataByProp', ]), ...mapState({ - withBatchComments: state => state.batchComments?.withBatchComments, + withBatchComments: (state) => state.batchComments?.withBatchComments, }), ...mapGetters('batchComments', ['hasDrafts']), showBatchCommentsActions() { @@ -125,8 +126,8 @@ export default { return ( this.discussion?.notes - .filter(n => n.resolvable) - .some(n => n.current_user?.can_resolve_discussion) || this.isDraft + .filter((n) => n.resolvable) + .some((n) => n.current_user?.can_resolve_discussion) || this.isDraft ); }, noteHash() { @@ -192,8 +193,7 @@ export default { }, canSuggest() { return ( - this.getNoteableData.can_receive_suggestion && - (this.line && this.line.can_receive_suggestion) + this.getNoteableData.can_receive_suggestion && this.line && this.line.can_receive_suggestion ); }, changedCommentText() { @@ -303,6 +303,9 @@ export default { this.$emit('handleFormUpdateAddToReview', this.updatedNoteBody, shouldResolve); }, + hasEmailParticipants() { + return this.getNoteableData.issue_email_participants?.length; + }, }, }; </script> @@ -316,46 +319,41 @@ export default { ></div> <div class="flash-container timeline-content"></div> <form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form"> - <noteable-warning - v-if="hasWarning(getNoteableData)" - :is-locked="isLocked(getNoteableData)" - :is-confidential="isConfidential(getNoteableData)" - :locked-noteable-docs-path="lockedIssueDocsPath" - :confidential-noteable-docs-path="confidentialIssueDocsPath" - /> - - <markdown-field - :markdown-preview-path="markdownPreviewPath" - :markdown-docs-path="markdownDocsPath" - :quick-actions-docs-path="quickActionsDocsPath" - :line="line" - :note="discussionNote" - :can-suggest="canSuggest" - :add-spacing-classes="false" - :help-page-path="helpPagePath" - :show-suggest-popover="showSuggestPopover" - :textarea-value="updatedNoteBody" - @handleSuggestDismissed="() => $emit('handleSuggestDismissed')" - > - <textarea - id="note_note" - ref="textarea" - slot="textarea" - v-model="updatedNoteBody" - :data-supports-quick-actions="!isEditing" - name="note[note]" - class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form" - data-qa-selector="reply_field" - dir="auto" - :aria-label="__('Description')" - :placeholder="__('Write a comment or drag your files hereā¦')" - @keydown.meta.enter="handleKeySubmit()" - @keydown.ctrl.enter="handleKeySubmit()" - @keydown.exact.up="editMyLastNote()" - @keydown.exact.esc="cancelHandler(true)" - @input="onInput" - ></textarea> - </markdown-field> + <comment-field-layout :noteable-data="getNoteableData"> + <markdown-field + :markdown-preview-path="markdownPreviewPath" + :markdown-docs-path="markdownDocsPath" + :quick-actions-docs-path="quickActionsDocsPath" + :line="line" + :note="discussionNote" + :can-suggest="canSuggest" + :add-spacing-classes="false" + :help-page-path="helpPagePath" + :show-suggest-popover="showSuggestPopover" + :textarea-value="updatedNoteBody" + @handleSuggestDismissed="() => $emit('handleSuggestDismissed')" + > + <template #textarea> + <textarea + id="note_note" + ref="textarea" + v-model="updatedNoteBody" + :data-supports-quick-actions="!isEditing && !glFeatures.tributeAutocomplete" + name="note[note]" + class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form" + data-qa-selector="reply_field" + dir="auto" + :aria-label="__('Description')" + :placeholder="__('Write a comment or drag your files hereā¦')" + @keydown.meta.enter="handleKeySubmit()" + @keydown.ctrl.enter="handleKeySubmit()" + @keydown.exact.up="editMyLastNote()" + @keydown.exact.esc="cancelHandler(true)" + @input="onInput" + ></textarea> + </template> + </markdown-field> + </comment-field-layout> <div class="note-form-actions clearfix"> <template v-if="showBatchCommentsActions"> <p v-if="showResolveDiscussionToggle"> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 62ee7f30c57..0a9a3da6069 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -201,14 +201,14 @@ export default { }; this.saveNote(replyData) - .then(res => { + .then((res) => { if (res.hasFlash !== true) { this.isReplying = false; clearDraft(this.autosaveKey); } callback(); }) - .catch(err => { + .catch((err) => { this.removePlaceholderNotes(); const msg = __( 'Your comment could not be submitted! Please check your network connection and try again.', diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 5073922e4a4..eaa64cf7c01 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -296,7 +296,7 @@ export default { this.updateSuccess(); callback(); }) - .catch(response => { + .catch((response) => { if (response.status === httpStatusCodes.GONE) { this.removeNote(this.note); this.updateSuccess(); diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 9eaa4e422d5..e9e687a8743 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -130,7 +130,7 @@ export default { const { parentElement } = this.$el; if (parentElement && parentElement.classList.contains('js-vue-notes-event')) { - parentElement.addEventListener('toggleAward', event => { + parentElement.addEventListener('toggleAward', (event) => { const { awardName, noteId } = event.detail; this.toggleAward({ awardName, noteId }); }); @@ -217,7 +217,7 @@ export default { const noteId = hash && hash.replace(/^note_/, ''); if (noteId) { - const discussion = this.discussions.find(d => d.notes.some(({ id }) => id === noteId)); + const discussion = this.discussions.find((d) => d.notes.some(({ id }) => id === noteId)); if (discussion) { this.expandDiscussion({ discussionId: discussion.id }); diff --git a/app/assets/javascripts/notes/components/timeline_toggle.vue b/app/assets/javascripts/notes/components/timeline_toggle.vue index d1ffe0a3601..8162878f80d 100644 --- a/app/assets/javascripts/notes/components/timeline_toggle.vue +++ b/app/assets/javascripts/notes/components/timeline_toggle.vue @@ -50,7 +50,6 @@ export default { v-gl-tooltip v-track-event="trackToggleTimelineView(timelineEnabled)" icon="comments" - size="small" :selected="timelineEnabled" :title="tooltip" :aria-label="tooltip" diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue index 0628e1d8647..ab7fa793bdc 100644 --- a/app/assets/javascripts/notes/components/toggle_replies_widget.vue +++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue @@ -26,9 +26,9 @@ export default { return this.replies[this.replies.length - 1]; }, uniqueAuthors() { - const authors = this.replies.map(reply => reply.author || {}); + const authors = this.replies.map((reply) => reply.author || {}); - return uniqBy(authors, author => author.username); + return uniqBy(authors, (author) => author.username); }, className() { return this.collapsed ? 'collapsed' : 'expanded'; diff --git a/app/assets/javascripts/notes/discussion_filters.js b/app/assets/javascripts/notes/discussion_filters.js index cdf9a46c5aa..7c9e7703d59 100644 --- a/app/assets/javascripts/notes/discussion_filters.js +++ b/app/assets/javascripts/notes/discussion_filters.js @@ -1,13 +1,13 @@ import Vue from 'vue'; import DiscussionFilter from './components/discussion_filter.vue'; -export default store => { +export default (store) => { const discussionFilterEl = document.getElementById('js-vue-discussion-filter'); if (discussionFilterEl) { const { defaultFilter, notesFilters } = discussionFilterEl.dataset; const filterValues = notesFilters ? JSON.parse(notesFilters) : {}; - const filters = Object.keys(filterValues).map(entry => ({ + const filters = Object.keys(filterValues).map((entry) => ({ title: entry, value: filterValues[entry], })); diff --git a/app/assets/javascripts/notes/mixins/diff_line_note_form.js b/app/assets/javascripts/notes/mixins/diff_line_note_form.js index c4a42eb1a98..5ce541781d4 100644 --- a/app/assets/javascripts/notes/mixins/diff_line_note_form.js +++ b/app/assets/javascripts/notes/mixins/diff_line_note_form.js @@ -9,9 +9,9 @@ import { formatLineRange } from '~/notes/components/multiline_comment_utils'; export default { computed: { ...mapState({ - noteableData: state => state.notes.noteableData, - notesData: state => state.notes.notesData, - withBatchComments: state => state.batchComments?.withBatchComments, + noteableData: (state) => state.notes.noteableData, + notesData: (state) => state.notes.notesData, + withBatchComments: (state) => state.batchComments?.withBatchComments, }), ...mapGetters('diffs', ['getDiffFileByHash']), ...mapGetters('batchComments', ['shouldRenderDraftRowInDiscussion', 'draftForDiscussion']), diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js index c6932bfacae..96974c4fa2d 100644 --- a/app/assets/javascripts/notes/mixins/discussion_navigation.js +++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js @@ -99,7 +99,7 @@ export default { 'getDiscussion', ]), ...mapState({ - currentDiscussionId: state => state.notes.currentDiscussionId, + currentDiscussionId: (state) => state.notes.currentDiscussionId, }), }, methods: { diff --git a/app/assets/javascripts/notes/mixins/issuable_state.js b/app/assets/javascripts/notes/mixins/issuable_state.js index 0ca8c8c98a3..52b67764b70 100644 --- a/app/assets/javascripts/notes/mixins/issuable_state.js +++ b/app/assets/javascripts/notes/mixins/issuable_state.js @@ -12,21 +12,10 @@ export default { lockedIssueDocsPath() { return this.getNoteableDataByProp('locked_discussion_docs_path'); }, - confidentialIssueDocsPath() { - return this.getNoteableDataByProp('confidential_issues_docs_path'); - }, }, methods: { - isConfidential(issue) { - return Boolean(issue.confidential); - }, - isLocked(issue) { return Boolean(issue.discussion_locked); }, - - hasWarning(issue) { - return this.isConfidential(issue) || this.isLocked(issue); - }, }, }; diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js index cef4475ed1d..baada4c5ce8 100644 --- a/app/assets/javascripts/notes/mixins/resolvable.js +++ b/app/assets/javascripts/notes/mixins/resolvable.js @@ -15,7 +15,7 @@ export default { if (notes) { // Decide resolved state using store. Only valid for discussions. - return notes.filter(note => !note.system).every(note => note.resolved); + return notes.filter((note) => !note.system).every((note) => note.resolved); } return resolved; diff --git a/app/assets/javascripts/notes/sort_discussions.js b/app/assets/javascripts/notes/sort_discussions.js index a06c23f5f76..ecfa3223039 100644 --- a/app/assets/javascripts/notes/sort_discussions.js +++ b/app/assets/javascripts/notes/sort_discussions.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import SortDiscussion from './components/sort_discussion.vue'; -export default store => { +export default (store) => { const el = document.getElementById('js-vue-sort-issue-discussions'); if (!el) return null; diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 1fe5d6c2955..c6684efed4d 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -141,7 +141,7 @@ export const updateNote = ({ commit, dispatch }, { endpoint, note }) => export const updateOrCreateNotes = ({ commit, state, getters, dispatch }, notes) => { const { notesById } = getters; - const debouncedFetchDiscussions = isFetching => { + const debouncedFetchDiscussions = (isFetching) => { if (!isFetching) { commit(types.SET_FETCHING_DISCUSSIONS, true); dispatch('fetchDiscussions', { path: state.notesData.discussionsPath }); @@ -159,7 +159,7 @@ export const updateOrCreateNotes = ({ commit, state, getters, dispatch }, notes) } }; - notes.forEach(note => { + notes.forEach((note) => { if (notesById[note.id]) { commit(types.UPDATE_NOTE, note); } else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) { @@ -329,7 +329,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { } } - const processQuickActions = res => { + const processQuickActions = (res) => { const { errors: { commands_only: message } = { commands_only: null } } = res; /* The following reply means that quick actions have been successfully applied: @@ -347,7 +347,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { return res; }; - const processEmojiAward = res => { + const processEmojiAward = (res) => { const { commands_changes: commandsChanges } = res; const { emoji_award: emojiAward } = commandsChanges || {}; if (!emojiAward) { @@ -357,7 +357,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { const votesBlock = $('.js-awards-block').eq(0); return loadAwardsHandler() - .then(awardsHandler => { + .then((awardsHandler) => { awardsHandler.addAwardToEmojiBar(votesBlock, emojiAward); awardsHandler.scrollToAwards(); }) @@ -371,7 +371,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { .then(() => res); }; - const processTimeTracking = res => { + const processTimeTracking = (res) => { const { commands_changes: commandsChanges } = res; const { spend_time: spendTime, time_estimate: timeEstimate } = commandsChanges || {}; if (spendTime != null || timeEstimate != null) { @@ -383,7 +383,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { return res; }; - const removePlaceholder = res => { + const removePlaceholder = (res) => { if (replyId) { commit(types.REMOVE_PLACEHOLDER_NOTES); } @@ -391,7 +391,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { return res; }; - const processErrors = error => { + const processErrors = (error) => { if (error.response) { const { response: { data = {} }, @@ -435,7 +435,7 @@ const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => { return resp; }; -const getFetchDataParams = state => { +const getFetchDataParams = (state) => { const endpoint = state.notesData.notesPath; const options = { headers: { @@ -559,7 +559,7 @@ export const updateResolvableDiscussionsCounts = ({ commit }) => export const submitSuggestion = ( { commit, dispatch }, - { discussionId, noteId, suggestionId, flashContainer }, + { discussionId, suggestionId, flashContainer, message }, ) => { const dispatchResolveDiscussion = () => dispatch('resolveDiscussion', { discussionId }).catch(() => {}); @@ -567,10 +567,9 @@ export const submitSuggestion = ( commit(types.SET_RESOLVING_DISCUSSION, true); dispatch('stopPolling'); - return Api.applySuggestion(suggestionId) - .then(() => commit(types.APPLY_SUGGESTION, { discussionId, noteId, suggestionId })) + return Api.applySuggestion(suggestionId, message) .then(dispatchResolveDiscussion) - .catch(err => { + .catch((err) => { const defaultMessage = __( 'Something went wrong while applying the suggestion. Please try again.', ); @@ -590,13 +589,8 @@ export const submitSuggestion = ( export const submitSuggestionBatch = ({ commit, dispatch, state }, { flashContainer }) => { const suggestionIds = state.batchSuggestionsInfo.map(({ suggestionId }) => suggestionId); - const applyAllSuggestions = () => - state.batchSuggestionsInfo.map(suggestionInfo => - commit(types.APPLY_SUGGESTION, suggestionInfo), - ); - const resolveAllDiscussions = () => - state.batchSuggestionsInfo.map(suggestionInfo => { + state.batchSuggestionsInfo.map((suggestionInfo) => { const { discussionId } = suggestionInfo; return dispatch('resolveDiscussion', { discussionId }).catch(() => {}); }); @@ -606,10 +600,9 @@ export const submitSuggestionBatch = ({ commit, dispatch, state }, { flashContai dispatch('stopPolling'); return Api.applySuggestionBatch(suggestionIds) - .then(() => Promise.all(applyAllSuggestions())) .then(() => Promise.all(resolveAllDiscussions())) .then(() => commit(types.CLEAR_SUGGESTION_BATCH)) - .catch(err => { + .catch((err) => { const defaultMessage = __( 'Something went wrong while applying the batch of suggestions. Please try again.', ); @@ -652,10 +645,10 @@ export const fetchDescriptionVersion = ({ dispatch }, { endpoint, startingVersio return axios .get(requestUrl) - .then(res => { + .then((res) => { dispatch('receiveDescriptionVersion', { descriptionVersion: res.data, versionId }); }) - .catch(error => { + .catch((error) => { dispatch('receiveDescriptionVersionError', error); Flash(__('Something went wrong while fetching description changes. Please try again.')); }); @@ -687,7 +680,7 @@ export const softDeleteDescriptionVersion = ( .then(() => { dispatch('receiveDeleteDescriptionVersion', versionId); }) - .catch(error => { + .catch((error) => { dispatch('receiveDeleteDescriptionVersionError', error); Flash(__('Something went wrong while deleting description changes. Please try again.')); diff --git a/app/assets/javascripts/notes/stores/collapse_utils.js b/app/assets/javascripts/notes/stores/collapse_utils.js index f34247d4eb0..b2e2f6e2c31 100644 --- a/app/assets/javascripts/notes/stores/collapse_utils.js +++ b/app/assets/javascripts/notes/stores/collapse_utils.js @@ -18,7 +18,7 @@ export const getTimeDifferenceMinutes = (noteBeggining, noteEnd) => { * @param {Object} note * @returns {Boolean} */ -export const isDescriptionSystemNote = note => note.system && note.note === DESCRIPTION_TYPE; +export const isDescriptionSystemNote = (note) => note.system && note.note === DESCRIPTION_TYPE; /** * Collapses the system notes of a description type, e.g. Changed the description, n minutes ago @@ -29,7 +29,7 @@ export const isDescriptionSystemNote = note => note.system && note.note === DESC * @param {Array} notes * @returns {Array} */ -export const collapseSystemNotes = notes => { +export const collapseSystemNotes = (notes) => { let lastDescriptionSystemNote = null; let lastDescriptionSystemNoteIndex = -1; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 5b3ffa425a0..5891a2e63e3 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -2,14 +2,14 @@ import { flattenDeep, clone } from 'lodash'; import * as constants from '../constants'; import { collapseSystemNotes } from './collapse_utils'; -export const discussions = state => { +export const discussions = (state) => { let discussionsInState = clone(state.discussions); // NOTE: not testing bc will be removed when backend is finished. if (state.isTimelineEnabled) { discussionsInState = discussionsInState .reduce((acc, discussion) => { - const transformedToIndividualNotes = discussion.notes.map(note => ({ + const transformedToIndividualNotes = discussion.notes.map((note) => ({ ...discussion, id: note.id, created_at: note.created_at, @@ -29,52 +29,52 @@ export const discussions = state => { return collapseSystemNotes(discussionsInState); }; -export const convertedDisscussionIds = state => state.convertedDisscussionIds; +export const convertedDisscussionIds = (state) => state.convertedDisscussionIds; -export const targetNoteHash = state => state.targetNoteHash; +export const targetNoteHash = (state) => state.targetNoteHash; -export const getNotesData = state => state.notesData; +export const getNotesData = (state) => state.notesData; -export const isNotesFetched = state => state.isNotesFetched; +export const isNotesFetched = (state) => state.isNotesFetched; /* * WARNING: This is an example of an "unnecessary" getter * more info found here: https://gitlab.com/groups/gitlab-org/-/epics/2913. */ -export const sortDirection = state => state.discussionSortOrder; +export const sortDirection = (state) => state.discussionSortOrder; -export const persistSortOrder = state => state.persistSortOrder; +export const persistSortOrder = (state) => state.persistSortOrder; -export const timelineEnabled = state => state.isTimelineEnabled; +export const timelineEnabled = (state) => state.isTimelineEnabled; -export const isLoading = state => state.isLoading; +export const isLoading = (state) => state.isLoading; -export const getNotesDataByProp = state => prop => state.notesData[prop]; +export const getNotesDataByProp = (state) => (prop) => state.notesData[prop]; -export const getNoteableData = state => state.noteableData; +export const getNoteableData = (state) => state.noteableData; -export const getNoteableDataByProp = state => prop => state.noteableData[prop]; +export const getNoteableDataByProp = (state) => (prop) => state.noteableData[prop]; -export const getBlockedByIssues = state => state.noteableData.blocked_by_issues; +export const getBlockedByIssues = (state) => state.noteableData.blocked_by_issues; -export const userCanReply = state => Boolean(state.noteableData.current_user.can_create_note); +export const userCanReply = (state) => Boolean(state.noteableData.current_user.can_create_note); -export const openState = state => state.noteableData.state; +export const openState = (state) => state.noteableData.state; -export const getUserData = state => state.userData || {}; +export const getUserData = (state) => state.userData || {}; -export const getUserDataByProp = state => prop => state.userData && state.userData[prop]; +export const getUserDataByProp = (state) => (prop) => state.userData && state.userData[prop]; -export const descriptionVersions = state => state.descriptionVersions; +export const descriptionVersions = (state) => state.descriptionVersions; -export const notesById = state => +export const notesById = (state) => state.discussions.reduce((acc, note) => { - note.notes.every(n => Object.assign(acc, { [n.id]: n })); + note.notes.every((n) => Object.assign(acc, { [n.id]: n })); return acc; }, {}); -export const noteableType = state => { +export const noteableType = (state) => { const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants; if (state.noteableData.noteableType === EPIC_NOTEABLE_TYPE) { @@ -84,21 +84,21 @@ export const noteableType = state => { return state.noteableData.merge_params ? MERGE_REQUEST_NOTEABLE_TYPE : ISSUE_NOTEABLE_TYPE; }; -const reverseNotes = array => array.slice(0).reverse(); +const reverseNotes = (array) => array.slice(0).reverse(); const isLastNote = (note, state) => !note.system && state.userData && note.author && note.author.id === state.userData.id; -export const getCurrentUserLastNote = state => - flattenDeep(reverseNotes(state.discussions).map(note => reverseNotes(note.notes))).find(el => +export const getCurrentUserLastNote = (state) => + flattenDeep(reverseNotes(state.discussions).map((note) => reverseNotes(note.notes))).find((el) => isLastNote(el, state), ); -export const getDiscussionLastNote = state => discussion => - reverseNotes(discussion.notes).find(el => isLastNote(el, state)); +export const getDiscussionLastNote = (state) => (discussion) => + reverseNotes(discussion.notes).find((el) => isLastNote(el, state)); -export const unresolvedDiscussionsCount = state => state.unresolvedDiscussionsCount; -export const resolvableDiscussionsCount = state => state.resolvableDiscussionsCount; +export const unresolvedDiscussionsCount = (state) => state.unresolvedDiscussionsCount; +export const resolvableDiscussionsCount = (state) => state.resolvableDiscussionsCount; export const showJumpToNextDiscussion = (state, getters) => (mode = 'discussion') => { const orderedDiffs = @@ -109,20 +109,20 @@ export const showJumpToNextDiscussion = (state, getters) => (mode = 'discussion' return orderedDiffs.length > 1; }; -export const isDiscussionResolved = (state, getters) => discussionId => +export const isDiscussionResolved = (state, getters) => (discussionId) => getters.resolvedDiscussionsById[discussionId] !== undefined; -export const allResolvableDiscussions = state => - state.discussions.filter(d => !d.individual_note && d.resolvable); +export const allResolvableDiscussions = (state) => + state.discussions.filter((d) => !d.individual_note && d.resolvable); -export const resolvedDiscussionsById = state => { +export const resolvedDiscussionsById = (state) => { const map = {}; state.discussions - .filter(d => d.resolvable) - .forEach(n => { + .filter((d) => d.resolvable) + .forEach((n) => { if (n.notes) { - const resolved = n.notes.filter(note => note.resolvable).every(note => note.resolved); + const resolved = n.notes.filter((note) => note.resolvable).every((note) => note.resolved); if (resolved) { map[n.id] = n; @@ -136,7 +136,7 @@ export const resolvedDiscussionsById = state => { // Gets Discussions IDs ordered by the date of their initial note export const unresolvedDiscussionsIdsByDate = (state, getters) => getters.allResolvableDiscussions - .filter(d => !d.resolved) + .filter((d) => !d.resolved) .sort((a, b) => { const aDate = new Date(a.notes[0].created_at); const bDate = new Date(b.notes[0].created_at); @@ -147,7 +147,7 @@ export const unresolvedDiscussionsIdsByDate = (state, getters) => return aDate === bDate ? 0 : 1; }) - .map(d => d.id); + .map((d) => d.id); // Gets Discussions IDs ordered by their position in the diff // @@ -156,7 +156,7 @@ export const unresolvedDiscussionsIdsByDate = (state, getters) => // line numbers. export const unresolvedDiscussionsIdsByDiff = (state, getters) => getters.allResolvableDiscussions - .filter(d => !d.resolved && d.active) + .filter((d) => !d.resolved && d.active) .sort((a, b) => { if (!a.diff_file || !b.diff_file) { return 0; @@ -176,7 +176,7 @@ export const unresolvedDiscussionsIdsByDiff = (state, getters) => ? -1 : 1; }) - .map(d => d.id); + .map((d) => d.id); export const resolvedDiscussionCount = (state, getters) => { const resolvedMap = getters.resolvedDiscussionsById; @@ -184,16 +184,16 @@ export const resolvedDiscussionCount = (state, getters) => { return Object.keys(resolvedMap).length; }; -export const discussionTabCounter = state => +export const discussionTabCounter = (state) => state.discussions.reduce( (acc, discussion) => - acc + discussion.notes.filter(note => !note.system && !note.placeholder).length, + acc + discussion.notes.filter((note) => !note.system && !note.placeholder).length, 0, ); // Returns the list of discussion IDs ordered according to given parameter // @param {Boolean} diffOrder - is ordered by diff? -export const unresolvedDiscussionsIdsOrdered = (state, getters) => diffOrder => { +export const unresolvedDiscussionsIdsOrdered = (state, getters) => (diffOrder) => { if (diffOrder) { return getters.unresolvedDiscussionsIdsByDiff; } @@ -241,17 +241,17 @@ export const previousUnresolvedDiscussionId = (state, getters) => (discussionId, getters.findUnresolvedDiscussionIdNeighbor({ discussionId, diffOrder, step: -1 }); // @param {Boolean} diffOrder - is ordered by diff? -export const firstUnresolvedDiscussionId = (state, getters) => diffOrder => { +export const firstUnresolvedDiscussionId = (state, getters) => (diffOrder) => { if (diffOrder) { return getters.unresolvedDiscussionsIdsByDiff[0]; } return getters.unresolvedDiscussionsIdsByDate[0]; }; -export const getDiscussion = state => discussionId => - state.discussions.find(discussion => discussion.id === discussionId); +export const getDiscussion = (state) => (discussionId) => + state.discussions.find((discussion) => discussion.id === discussionId); -export const commentsDisabled = state => state.commentsDisabled; +export const commentsDisabled = (state) => state.commentsDisabled; export const suggestionsCount = (state, getters) => - Object.values(getters.notesById).filter(n => n.suggestions.length).length; + Object.values(getters.notesById).filter((n) => n.suggestions.length).length; diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index 4421a84a6b1..144a3d7ba90 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -15,7 +15,7 @@ export default () => ({ batchSuggestionsInfo: [], currentlyFetchingDiscussions: false, /** - * selectedCommentPosition & selectedCommentPosition structures are the same as `position.line_range`: + * selectedCommentPosition & selectedCommentPositionHover structures are the same as `position.line_range`: * { * start: { line_code: string, new_line: number, old_line:number, type: string }, * end: { line_code: string, new_line: number, old_line:number, type: string }, diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 53387b2eaff..2c51ce0d970 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -7,7 +7,7 @@ export default { [types.ADD_NEW_NOTE](state, data) { const note = data.discussion ? data.discussion.notes[0] : data; const { discussion_id, type } = note; - const [exists] = state.discussions.filter(n => n.id === note.discussion_id); + const [exists] = state.discussions.filter((n) => n.id === note.discussion_id); const isDiscussion = type === constants.DISCUSSION_NOTE || type === constants.DIFF_NOTE; if (!exists) { @@ -128,7 +128,7 @@ export default { // To support legacy notes, should be very rare case. if (discussion.individual_note && discussion.notes.length > 1) { - discussion.notes.forEach(n => { + discussion.notes.forEach((n) => { acc.push({ ...discussion, ...diffData, @@ -183,7 +183,7 @@ export default { const { id, name, username } = state.userData; const hasEmojiAwardedByCurrentUser = note.award_emoji.filter( - emoji => `${emoji.name}` === `${data.awardName}` && emoji.user.id === id, + (emoji) => `${emoji.name}` === `${data.awardName}` && emoji.user.id === id, ); if (hasEmojiAwardedByCurrentUser.length) { @@ -206,7 +206,7 @@ export default { [types.SET_EXPAND_DISCUSSIONS](state, { discussionIds, expanded }) { if (discussionIds?.length) { - discussionIds.forEach(discussionId => { + discussionIds.forEach((discussionId) => { const discussion = utils.findNoteObjectById(state.discussions, discussionId); Object.assign(discussion, { expanded }); }); @@ -236,7 +236,7 @@ export default { const noteObj = utils.findNoteObjectById(state.discussions, discussionId); const comment = utils.findNoteObjectById(noteObj.notes, noteId); - comment.suggestions = comment.suggestions.map(suggestion => ({ + comment.suggestions = comment.suggestions.map((suggestion) => ({ ...suggestion, applied: suggestion.applied || suggestion.id === suggestionId, appliable: false, @@ -244,13 +244,13 @@ export default { }, [types.SET_APPLYING_BATCH_STATE](state, isApplyingBatch) { - state.batchSuggestionsInfo.forEach(suggestionInfo => { + state.batchSuggestionsInfo.forEach((suggestionInfo) => { const { discussionId, noteId, suggestionId } = suggestionInfo; const noteObj = utils.findNoteObjectById(state.discussions, discussionId); const comment = utils.findNoteObjectById(noteObj.notes, noteId); - comment.suggestions = comment.suggestions.map(suggestion => ({ + comment.suggestions = comment.suggestions.map((suggestion) => ({ ...suggestion, is_applying_batch: suggestion.id === suggestionId && isApplyingBatch, })); @@ -278,7 +278,7 @@ export default { [types.UPDATE_DISCUSSION](state, noteData) { const note = noteData; - const selectedDiscussion = state.discussions.find(disc => disc.id === note.id); + const selectedDiscussion = state.discussions.find((disc) => disc.id === note.id); note.expanded = true; // override expand flag to prevent collapse if (note.diff_file) { Object.assign(note, { @@ -289,7 +289,7 @@ export default { }, [types.UPDATE_DISCUSSION_POSITION](state, { discussionId, position }) { - const selectedDiscussion = state.discussions.find(disc => disc.id === discussionId); + const selectedDiscussion = state.discussions.find((disc) => disc.id === discussionId); if (selectedDiscussion) Object.assign(selectedDiscussion.position, { ...position }); }, @@ -341,13 +341,13 @@ export default { }, [types.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS](state) { state.resolvableDiscussionsCount = state.discussions.filter( - discussion => !discussion.individual_note && discussion.resolvable, + (discussion) => !discussion.individual_note && discussion.resolvable, ).length; state.unresolvedDiscussionsCount = state.discussions.filter( - discussion => + (discussion) => !discussion.individual_note && discussion.resolvable && - discussion.notes.some(note => note.resolvable && !note.resolved), + discussion.notes.some((note) => note.resolvable && !note.resolved), ).length; }, diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js index 10faac0c32b..6df926e1249 100644 --- a/app/assets/javascripts/notes/stores/utils.js +++ b/app/assets/javascripts/notes/stores/utils.js @@ -6,13 +6,13 @@ import createGqClient, { fetchPolicies } from '~/lib/graphql'; // factory function because global flag makes RegExp stateful const createQuickActionsRegex = () => /^\/\w+.*$/gm; -export const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0]; +export const findNoteObjectById = (notes, id) => notes.filter((n) => n.id === id)[0]; -export const getQuickActionText = note => { +export const getQuickActionText = (note) => { let text = __('Applying command'); const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || []; - const executedCommands = quickActions.filter(command => { + const executedCommands = quickActions.filter((command) => { const commandRegex = new RegExp(`/${command.name}`); return commandRegex.test(note); }); @@ -29,12 +29,12 @@ export const getQuickActionText = note => { return text; }; -export const hasQuickActions = note => createQuickActionsRegex().test(note); +export const hasQuickActions = (note) => createQuickActionsRegex().test(note); -export const stripQuickActions = note => note.replace(createQuickActionsRegex(), '').trim(); +export const stripQuickActions = (note) => note.replace(createQuickActionsRegex(), '').trim(); -export const prepareDiffLines = diffLines => - diffLines.map(line => ({ ...trimFirstCharOfLineContent(line) })); +export const prepareDiffLines = (diffLines) => + diffLines.map((line) => ({ ...trimFirstCharOfLineContent(line) })); export const gqClient = createGqClient( {}, diff --git a/app/assets/javascripts/notes/utils.js b/app/assets/javascripts/notes/utils.js index e6c2eb06a51..7966a884eab 100644 --- a/app/assets/javascripts/notes/utils.js +++ b/app/assets/javascripts/notes/utils.js @@ -4,7 +4,7 @@ * Tracks snowplow event when User toggles timeline view * @param {Boolean} enabled that will be send as a property for the event */ -export const trackToggleTimelineView = enabled => ({ +export const trackToggleTimelineView = (enabled) => ({ category: 'Incident Management', action: 'toggle_incident_comments_into_timeline_view', label: 'Status', diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js index ae992dd5dc5..eaa5ec3a2e4 100644 --- a/app/assets/javascripts/notifications_dropdown.js +++ b/app/assets/javascripts/notifications_dropdown.js @@ -12,9 +12,7 @@ export default function notificationsDropdown() { } const notificationLevel = $(this).data('notificationLevel'); - const form = $(this) - .parents('.notification-form') - .first(); + const form = $(this).parents('.notification-form').first(); form.find('.js-notification-loading').toggleClass('spinner'); if (form.hasClass('no-label')) { @@ -25,13 +23,11 @@ export default function notificationsDropdown() { Rails.fire(form[0], 'submit'); }); - $(document).on('ajax:success', '.notification-form', e => { + $(document).on('ajax:success', '.notification-form', (e) => { const data = e.detail[0]; if (data.saved) { - $(e.currentTarget) - .closest('.js-notification-dropdown') - .replaceWith(data.html); + $(e.currentTarget).closest('.js-notification-dropdown').replaceWith(data.html); } else { Flash(__('Failed to save new settings'), 'alert'); } diff --git a/app/assets/javascripts/onboarding_issues/index.js b/app/assets/javascripts/onboarding_issues/index.js index 27f2f7f0e9d..b23a10c9254 100644 --- a/app/assets/javascripts/onboarding_issues/index.js +++ b/app/assets/javascripts/onboarding_issues/index.js @@ -57,7 +57,7 @@ const showPopover = (el, path, footer, options) => { .popover('show'); // The previous popover actions have been taken, don't show those popovers anymore. - Object.keys(settings).forEach(pathSetting => { + Object.keys(settings).forEach((pathSetting) => { if (path !== pathSetting) { settings[pathSetting] = false; } else { @@ -71,7 +71,7 @@ const showPopover = (el, path, footer, options) => { } }; -export const showLearnGitLabGroupItemPopover = id => { +export const showLearnGitLabGroupItemPopover = (id) => { const el = document.querySelector(`#group-${id} .group-text a`); if (!el) return; diff --git a/app/assets/javascripts/operation_settings/store/actions.js b/app/assets/javascripts/operation_settings/store/actions.js index 1d3adeefbd8..bbbff6630d3 100644 --- a/app/assets/javascripts/operation_settings/store/actions.js +++ b/app/assets/javascripts/operation_settings/store/actions.js @@ -21,7 +21,7 @@ export const saveChanges = ({ state, dispatch }) => }, }) .then(() => dispatch('receiveSaveChangesSuccess')) - .catch(error => dispatch('receiveSaveChangesError', error)); + .catch((error) => dispatch('receiveSaveChangesError', error)); export const receiveSaveChangesSuccess = () => { /** diff --git a/app/assets/javascripts/operation_settings/store/index.js b/app/assets/javascripts/operation_settings/store/index.js index e96bb1e8aad..e3dcfd31a83 100644 --- a/app/assets/javascripts/operation_settings/store/index.js +++ b/app/assets/javascripts/operation_settings/store/index.js @@ -6,7 +6,7 @@ import mutations from './mutations'; Vue.use(Vuex); -export const createStore = initialState => +export const createStore = (initialState) => new Vuex.Store({ state: createState(initialState), actions, diff --git a/app/assets/javascripts/packages/details/components/package_files.vue b/app/assets/javascripts/packages/details/components/package_files.vue index ab46dd0114d..3add454fda3 100644 --- a/app/assets/javascripts/packages/details/components/package_files.vue +++ b/app/assets/javascripts/packages/details/components/package_files.vue @@ -25,14 +25,14 @@ export default { }, computed: { filesTableRows() { - return this.packageFiles.map(pf => ({ + return this.packageFiles.map((pf) => ({ ...pf, size: this.formatSize(pf.size), pipeline: last(pf.pipelines), })); }, showCommitColumn() { - return this.filesTableRows.some(row => Boolean(row.pipeline?.id)); + return this.filesTableRows.some((row) => Boolean(row.pipeline?.id)); }, filesTableHeaderFields() { return [ @@ -55,7 +55,7 @@ export default { label: __('Created'), class: 'gl-text-right', }, - ].filter(c => !c.hide); + ].filter((c) => !c.hide); }, }, methods: { @@ -90,7 +90,7 @@ export default { </gl-link> </template> - <template #cell(commit)="{item}"> + <template #cell(commit)="{ item }"> <gl-link :href="item.pipeline.project.commit_url" class="gl-text-gray-500" diff --git a/app/assets/javascripts/packages/list/components/packages_list.vue b/app/assets/javascripts/packages/list/components/packages_list.vue index 7067f70a923..1e38ee525b8 100644 --- a/app/assets/javascripts/packages/list/components/packages_list.vue +++ b/app/assets/javascripts/packages/list/components/packages_list.vue @@ -24,10 +24,10 @@ export default { }, computed: { ...mapState({ - perPage: state => state.pagination.perPage, - totalItems: state => state.pagination.total, - page: state => state.pagination.page, - isGroupPage: state => state.config.isGroupPage, + perPage: (state) => state.pagination.perPage, + totalItems: (state) => state.pagination.total, + page: (state) => state.pagination.page, + isGroupPage: (state) => state.config.isGroupPage, isLoading: 'isLoading', }), ...mapGetters({ list: 'getList' }), diff --git a/app/assets/javascripts/packages/list/components/packages_list_app.vue b/app/assets/javascripts/packages/list/components/packages_list_app.vue index cbb3bfd35ac..2a786b92515 100644 --- a/app/assets/javascripts/packages/list/components/packages_list_app.vue +++ b/app/assets/javascripts/packages/list/components/packages_list_app.vue @@ -25,12 +25,12 @@ export default { }, computed: { ...mapState({ - emptyListIllustration: state => state.config.emptyListIllustration, - emptyListHelpUrl: state => state.config.emptyListHelpUrl, - filterQuery: state => state.filterQuery, - selectedType: state => state.selectedType, - packageHelpUrl: state => state.config.packageHelpUrl, - packagesCount: state => state.pagination?.total, + emptyListIllustration: (state) => state.config.emptyListIllustration, + emptyListHelpUrl: (state) => state.config.emptyListHelpUrl, + filterQuery: (state) => state.filterQuery, + selectedType: (state) => state.selectedType, + packageHelpUrl: (state) => state.config.packageHelpUrl, + packagesCount: (state) => state.pagination?.total, }), tabsToRender() { return PACKAGE_REGISTRY_TABS; @@ -110,7 +110,7 @@ export default { <template #description> <gl-sprintf v-if="filterQuery" :message="$options.i18n.widenFilters" /> <gl-sprintf v-else :message="$options.i18n.noResults"> - <template #noPackagesLink="{content}"> + <template #noPackagesLink="{ content }"> <gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link> </template> </gl-sprintf> diff --git a/app/assets/javascripts/packages/list/components/packages_sort.vue b/app/assets/javascripts/packages/list/components/packages_sort.vue index 47e51bbdca5..4b2d9091f8f 100644 --- a/app/assets/javascripts/packages/list/components/packages_sort.vue +++ b/app/assets/javascripts/packages/list/components/packages_sort.vue @@ -12,12 +12,12 @@ export default { }, computed: { ...mapState({ - isGroupPage: state => state.config.isGroupPage, - orderBy: state => state.sorting.orderBy, - sort: state => state.sorting.sort, + isGroupPage: (state) => state.config.isGroupPage, + orderBy: (state) => state.sorting.orderBy, + sort: (state) => state.sorting.sort, }), sortText() { - const field = this.sortableFields.find(s => s.orderBy === this.orderBy); + const field = this.sortableFields.find((s) => s.orderBy === this.orderBy); return field ? field.label : ''; }, sortableFields() { diff --git a/app/assets/javascripts/packages/list/stores/getters.js b/app/assets/javascripts/packages/list/stores/getters.js index 0af7e453f19..85c489deda0 100644 --- a/app/assets/javascripts/packages/list/stores/getters.js +++ b/app/assets/javascripts/packages/list/stores/getters.js @@ -1,5 +1,5 @@ import { LIST_KEY_PROJECT } from '../constants'; import { beautifyPath } from '../../shared/utils'; -export default state => - state.packages.map(p => ({ ...p, projectPathName: beautifyPath(p[LIST_KEY_PROJECT]) })); +export default (state) => + state.packages.map((p) => ({ ...p, projectPathName: beautifyPath(p[LIST_KEY_PROJECT]) })); diff --git a/app/assets/javascripts/packages/list/utils.js b/app/assets/javascripts/packages/list/utils.js index 6a300d7bfe6..ee89d3cdefe 100644 --- a/app/assets/javascripts/packages/list/utils.js +++ b/app/assets/javascripts/packages/list/utils.js @@ -1,6 +1,7 @@ import { LIST_KEY_PROJECT, SORT_FIELDS } from './constants'; -export default isGroupPage => SORT_FIELDS.filter(f => f.key !== LIST_KEY_PROJECT || isGroupPage); +export default (isGroupPage) => + SORT_FIELDS.filter((f) => f.key !== LIST_KEY_PROJECT || isGroupPage); /** * A small util function that works out if the delete action has deleted the diff --git a/app/assets/javascripts/packages/shared/components/package_tags.vue b/app/assets/javascripts/packages/shared/components/package_tags.vue index 3d7e233c1ba..5172b855fc3 100644 --- a/app/assets/javascripts/packages/shared/components/package_tags.vue +++ b/app/assets/javascripts/packages/shared/components/package_tags.vue @@ -43,7 +43,7 @@ export default { if (this.moreTagsDisplay) { return this.tags .slice(this.tagDisplayLimit) - .map(x => x.name) + .map((x) => x.name) .join(', '); } diff --git a/app/assets/javascripts/packages/shared/utils.js b/app/assets/javascripts/packages/shared/utils.js index d7a883e4397..677550f77ec 100644 --- a/app/assets/javascripts/packages/shared/utils.js +++ b/app/assets/javascripts/packages/shared/utils.js @@ -1,13 +1,13 @@ import { s__ } from '~/locale'; import { PackageType, TrackingCategories } from './constants'; -export const packageTypeToTrackCategory = type => +export const packageTypeToTrackCategory = (type) => // eslint-disable-next-line @gitlab/require-i18n-strings `UI::${TrackingCategories[type]}`; -export const beautifyPath = path => (path ? path.split('/').join(' / ') : ''); +export const beautifyPath = (path) => (path ? path.split('/').join(' / ') : ''); -export const getPackageTypeLabel = packageType => { +export const getPackageTypeLabel = (packageType) => { switch (packageType) { case PackageType.CONAN: return s__('PackageType|Conan'); diff --git a/app/assets/javascripts/packages_and_registries/settings/group/bundle.js b/app/assets/javascripts/packages_and_registries/settings/group/bundle.js new file mode 100644 index 00000000000..a3d507180c6 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/group/bundle.js @@ -0,0 +1,20 @@ +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; +import SettingsApp from './components/group_settings_app.vue'; +import { apolloProvider } from './graphql'; + +Vue.use(Translate); + +export default () => { + const el = document.getElementById('js-packages-and-registries-settings'); + if (!el) { + return null; + } + return new Vue({ + el, + apolloProvider, + render(createElement) { + return createElement(SettingsApp); + }, + }); +}; diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue new file mode 100644 index 00000000000..6bcecf43a13 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue @@ -0,0 +1,9 @@ +<script> +export default { + name: 'GroupSettingsApp', +}; +</script> + +<template> + <section></section> +</template> diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/index.js b/app/assets/javascripts/packages_and_registries/settings/group/graphql/index.js new file mode 100644 index 00000000000..16152eb81f6 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/index.js @@ -0,0 +1,14 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; + +Vue.use(VueApollo); + +export const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient( + {}, + { + assumeImmutableResults: true, + }, + ), +}); diff --git a/app/assets/javascripts/pages/admin/admin.js b/app/assets/javascripts/pages/admin/admin.js index 038bbe392ba..e92262852cf 100644 --- a/app/assets/javascripts/pages/admin/admin.js +++ b/app/assets/javascripts/pages/admin/admin.js @@ -23,7 +23,7 @@ export default function adminInit() { } }); - $('body').on('click', '.js-toggle-colors-link', e => { + $('body').on('click', '.js-toggle-colors-link', (e) => { e.preventDefault(); $('.js-toggle-colors-container').toggleClass('hide'); }); @@ -33,7 +33,7 @@ export default function adminInit() { $(this).tab('show'); }); - $('.log-bottom').on('click', e => { + $('.log-bottom').on('click', (e) => { e.preventDefault(); const $visibleLog = $('.file-content:visible'); @@ -52,7 +52,7 @@ export default function adminInit() { modal.show(); }); - $('.change-owner-cancel-link').on('click', e => { + $('.change-owner-cancel-link').on('click', (e) => { e.preventDefault(); modal.hide(); $('.change-owner-link').show(); diff --git a/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js b/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js index f8fc53799a8..b995cb1d3dd 100644 --- a/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js +++ b/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js @@ -14,7 +14,7 @@ export default class PayloadPreviewer { this.spinner = this.trigger.querySelector('.js-spinner'); this.text = this.trigger.querySelector('.js-text'); - this.trigger.addEventListener('click', event => { + this.trigger.addEventListener('click', (event) => { event.preventDefault(); if (this.isVisible) return this.hidePayload(); diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js index a75f5d318a0..9e4c4d9f615 100644 --- a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js +++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js @@ -82,7 +82,7 @@ export default () => { return $jsBroadcastMessagePreview.css(selectedColorStyle); }; - const setSuggestedColor = e => { + const setSuggestedColor = (e) => { const color = $(e.currentTarget).data('color'); $broadcastMessageColor .val(color) diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue index 4b6f52c09be..d65593963ce 100644 --- a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue +++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue @@ -26,11 +26,11 @@ export default { onSubmit() { return axios .post(this.url) - .then(response => { + .then((response) => { // follow the rediect to refresh the page redirectTo(response.request.responseURL); }) - .catch(error => { + .catch((error) => { createFlash(s__('AdminArea|Stopping jobs failed')); throw error; }); diff --git a/app/assets/javascripts/pages/admin/projects/index.js b/app/assets/javascripts/pages/admin/projects/index.js index d86c5e2ddb8..fa2b0546c02 100644 --- a/app/assets/javascripts/pages/admin/projects/index.js +++ b/app/assets/javascripts/pages/admin/projects/index.js @@ -24,5 +24,5 @@ document.addEventListener('DOMContentLoaded', () => { document .querySelectorAll('.js-namespace-select') - .forEach(dropdown => new NamespaceSelect({ dropdown })); + .forEach((dropdown) => new NamespaceSelect({ dropdown })); }); diff --git a/app/assets/javascripts/pages/admin/projects/index/index.js b/app/assets/javascripts/pages/admin/projects/index/index.js index ebb1a74e970..bf512ef395d 100644 --- a/app/assets/javascripts/pages/admin/projects/index/index.js +++ b/app/assets/javascripts/pages/admin/projects/index/index.js @@ -18,7 +18,7 @@ document.addEventListener('DOMContentLoaded', () => { }, mounted() { const deleteProjectButtons = document.querySelectorAll('.delete-project-button'); - deleteProjectButtons.forEach(button => { + deleteProjectButtons.forEach((button) => { button.addEventListener('click', () => { const buttonProps = button.dataset; deleteModal.deleteProjectUrl = buttonProps.deleteProjectUrl; diff --git a/app/assets/javascripts/pages/admin/users/index.js b/app/assets/javascripts/pages/admin/users/index.js index 07462b4592f..75a8284f5f8 100644 --- a/app/assets/javascripts/pages/admin/users/index.js +++ b/app/assets/javascripts/pages/admin/users/index.js @@ -17,7 +17,7 @@ function loadModalsConfigurationFromHtml(modalsElement) { throw new Error('Modals content element not found!'); } - Array.from(modalsElement.children).forEach(node => { + Array.from(modalsElement.children).forEach((node) => { const { modal, ...config } = node.dataset; modalsConfiguration[modal] = { title: node.dataset.title, diff --git a/app/assets/javascripts/pages/admin/users/new/index.js b/app/assets/javascripts/pages/admin/users/new/index.js index 3e6a090cb0e..7b7d4c169ef 100644 --- a/app/assets/javascripts/pages/admin/users/new/index.js +++ b/app/assets/javascripts/pages/admin/users/new/index.js @@ -15,7 +15,7 @@ export default class UserInternalRegexHandler { } addListenerToEmailField() { - $('#user_email').on('input', event => { + $('#user_email').on('input', (event) => { this.setExternalCheckbox(event.currentTarget.value); }); } diff --git a/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue b/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue index 9fa441348c7..bed753b0c40 100644 --- a/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue +++ b/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue @@ -57,7 +57,7 @@ export default { .post(this.calloutsPath, { feature_name: this.calloutsFeatureId, }) - .catch(e => { + .catch((e) => { // eslint-disable-next-line @gitlab/require-i18n-strings, no-console console.error('Failed to dismiss banner.', e); }); diff --git a/app/assets/javascripts/pages/dashboard/projects/index/init_customize_homepage_banner.js b/app/assets/javascripts/pages/dashboard/projects/index/init_customize_homepage_banner.js index c0735dde1da..8cdcd3134ee 100644 --- a/app/assets/javascripts/pages/dashboard/projects/index/init_customize_homepage_banner.js +++ b/app/assets/javascripts/pages/dashboard/projects/index/init_customize_homepage_banner.js @@ -11,6 +11,6 @@ export default () => { return new Vue({ el, provide: { ...el.dataset }, - render: createElement => createElement(CustomizeHomepageBanner), + render: (createElement) => createElement(CustomizeHomepageBanner), }); }; diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js index 6f8d954d798..bd283201eff 100644 --- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js @@ -154,7 +154,7 @@ export default class Todos { goToTodoUrl(e) { const todoLink = this.dataset.url; - if (!todoLink || e.target.tagName === 'A' || e.target.tagName === 'IMG') { + if (!todoLink || e.target.closest('a')) { return; } diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js index d3900b84fa7..5346e3720e8 100644 --- a/app/assets/javascripts/pages/groups/group_members/index.js +++ b/app/assets/javascripts/pages/groups/group_members/index.js @@ -1,10 +1,11 @@ import Vue from 'vue'; -import Members from 'ee_else_ce/members'; import memberExpirationDate from '~/member_expiration_date'; import UsersSelect from '~/users_select'; import groupsSelect from '~/groups_select'; import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; import { initGroupMembersApp } from '~/groups/members'; +import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; +import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; import { memberRequestFormatter, groupLinkRequestFormatter } from '~/groups/members/utils'; import { s__ } from '~/locale'; @@ -65,6 +66,7 @@ groupsSelect(); memberExpirationDate(); memberExpirationDate('.js-access-expiration-date-groups'); mountRemoveMemberModal(); +initInviteMembersModal(); +initInviteMembersTrigger(); -new Members(); // eslint-disable-line no-new new UsersSelect(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js index 4d0a03e151a..5cb21ca61ab 100644 --- a/app/assets/javascripts/pages/groups/issues/index.js +++ b/app/assets/javascripts/pages/groups/issues/index.js @@ -9,6 +9,7 @@ import initManualOrdering from '~/manual_ordering'; const ISSUE_BULK_UPDATE_PREFIX = 'issue_'; IssuableFilteredSearchTokenKeys.addExtraTokensForIssues(); +IssuableFilteredSearchTokenKeys.removeTokensForKeys('release'); issuableInitBulkUpdateSidebar.init(ISSUE_BULK_UPDATE_PREFIX); initIssuablesList(); diff --git a/app/assets/javascripts/pages/groups/new/group_path_validator.js b/app/assets/javascripts/pages/groups/new/group_path_validator.js index 83b38b0f1a5..97f3d8cf7f5 100644 --- a/app/assets/javascripts/pages/groups/new/group_path_validator.js +++ b/app/assets/javascripts/pages/groups/new/group_path_validator.js @@ -21,11 +21,11 @@ export default class GroupPathValidator extends InputValidator { const container = opts.container || ''; const validateElements = document.querySelectorAll(`${container} .js-validate-group-path`); - this.debounceValidateInput = debounce(inputDomElement => { + this.debounceValidateInput = debounce((inputDomElement) => { GroupPathValidator.validateGroupPathInput(inputDomElement); }, debounceTimeoutDuration); - validateElements.forEach(element => + validateElements.forEach((element) => element.addEventListener('input', this.eventHandler.bind(this)), ); } @@ -45,7 +45,7 @@ export default class GroupPathValidator extends InputValidator { fetchGroupPathAvailability(groupPath) .then(({ data }) => data) - .then(data => { + .then((data) => { GroupPathValidator.setInputState(inputDomElement, !data.exists); GroupPathValidator.setMessageVisibility(inputDomElement, pendingMessageSelector, false); GroupPathValidator.setMessageVisibility( diff --git a/app/assets/javascripts/pages/groups/settings/packages_and_registries/index.js b/app/assets/javascripts/pages/groups/settings/packages_and_registries/index.js new file mode 100644 index 00000000000..3b922622d2c --- /dev/null +++ b/app/assets/javascripts/pages/groups/settings/packages_and_registries/index.js @@ -0,0 +1,3 @@ +import bundle from '~/packages_and_registries/settings/group/bundle'; + +bundle(); diff --git a/app/assets/javascripts/pages/groups/shared/group_tabs.js b/app/assets/javascripts/pages/groups/shared/group_tabs.js index c6fe61d2bd9..033843d8504 100644 --- a/app/assets/javascripts/pages/groups/shared/group_tabs.js +++ b/app/assets/javascripts/pages/groups/shared/group_tabs.js @@ -20,7 +20,7 @@ export default class GroupTabs extends UserTabs { bindEvents() { this.$parentEl .off('shown.bs.tab', '.nav-links a[data-toggle="tab"]') - .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event)); + .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', (event) => this.tabShown(event)); } tabShown(event) { @@ -117,7 +117,7 @@ export default class GroupTabs extends UserTabs { cleanFilterState() { const values = Object.values(this.loaded); - const loadedTabs = values.filter(e => e === true); + const loadedTabs = values.filter((e) => e === true); if (!loadedTabs.length) { return; diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue index 93fe38831be..f47945c5a9f 100644 --- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue +++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue @@ -82,7 +82,7 @@ Once deleted, it cannot be undone or recovered.`), return axios .delete(this.milestoneUrl) - .then(response => { + .then((response) => { eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: true, @@ -91,7 +91,7 @@ Once deleted, it cannot be undone or recovered.`), // follow the rediect to milestones overview page redirectTo(response.request.responseURL); }) - .catch(error => { + .catch((error) => { eventHub.$emit('deleteMilestoneModal.requestFinished', { milestoneUrl: this.milestoneUrl, successful: false, diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue index 0dc54b612ba..ecde11aff40 100644 --- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue +++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue @@ -1,28 +1,22 @@ <script> +import { GlModal } from '@gitlab/ui'; import axios from '~/lib/utils/axios_utils'; import { deprecatedCreateFlash as createFlash } from '~/flash'; -import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue'; import { s__, sprintf } from '~/locale'; import { visitUrl } from '~/lib/utils/url_utility'; -import eventHub from '../event_hub'; export default { components: { - GlModal: DeprecatedModal2, + GlModal, }, - props: { - milestoneTitle: { - type: String, - required: true, - }, - url: { - type: String, - required: true, - }, - groupName: { - type: String, - required: true, - }, + data() { + return { + milestoneTitle: '', + url: '', + groupName: '', + currentButton: null, + visible: false, + }; }, computed: { title() { @@ -38,42 +32,71 @@ export default { ); }, }, + mounted() { + this.getButtons().forEach((button) => { + button.addEventListener('click', this.onPromoteButtonClick); + button.removeAttribute('disabled'); + }); + }, + beforeDestroy() { + this.getButtons().forEach((button) => { + button.removeEventListener('click', this.onPromoteButtonClick); + }); + }, methods: { + onPromoteButtonClick({ currentTarget }) { + const { milestoneTitle, url, groupName } = currentTarget.dataset; + currentTarget.setAttribute('disabled', ''); + this.visible = true; + this.milestoneTitle = milestoneTitle; + this.url = url; + this.groupName = groupName; + this.currentButton = currentTarget; + }, + getButtons() { + return document.querySelectorAll('.js-promote-project-milestone-button'); + }, onSubmit() { - eventHub.$emit('promoteMilestoneModal.requestStarted', this.url); return axios .post(this.url, { params: { format: 'json' } }) - .then(response => { - eventHub.$emit('promoteMilestoneModal.requestFinished', { - milestoneUrl: this.url, - successful: true, - }); + .then((response) => { visitUrl(response.data.url); }) - .catch(error => { - eventHub.$emit('promoteMilestoneModal.requestFinished', { - milestoneUrl: this.url, - successful: false, - }); + .catch((error) => { createFlash(error); + }) + .finally(() => { + this.visible = false; }); }, + onClose() { + this.visible = false; + if (this.currentButton) { + this.currentButton.removeAttribute('disabled'); + } + }, + }, + primaryAction: { + text: s__('Milestones|Promote Milestone'), + attributes: [{ variant: 'warning' }], + }, + cancelAction: { + text: s__('Cancel'), + attributes: [], }, }; </script> <template> <gl-modal - id="promote-milestone-modal" - :footer-primary-button-text="s__('Milestones|Promote Milestone')" - footer-primary-button-variant="warning" - @submit="onSubmit" + :visible="visible" + modal-id="promote-milestone-modal" + :action-primary="$options.primaryAction" + :action-cancel="$options.cancelAction" + :title="title" + @primary="onSubmit" + @hide="onClose" > - <template #title> - {{ title }} - </template> - <div> - <p>{{ text }}</p> - <p>{{ s__('Milestones|This action cannot be reversed.') }}</p> - </div> + <p>{{ text }}</p> + <p>{{ s__('Milestones|This action cannot be reversed.') }}</p> </gl-modal> </template> diff --git a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js index 6e68114e04b..e8b67891c42 100644 --- a/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js +++ b/app/assets/javascripts/pages/milestones/shared/delete_milestone_modal_init.js @@ -20,7 +20,7 @@ export default () => { const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button'); - const onRequestStarted = milestoneUrl => { + const onRequestStarted = (milestoneUrl) => { const button = document.querySelector( `.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`, ); @@ -44,7 +44,7 @@ export default () => { }, mounted() { eventHub.$on('deleteMilestoneModal.props', this.setModalProps); - deleteMilestoneButtons.forEach(button => { + deleteMilestoneButtons.forEach((button) => { button.removeAttribute('disabled'); button.addEventListener('click', () => { this.$root.$emit('bv::show::modal', 'delete-milestone-modal'); diff --git a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js index fcc62a2b2af..5472b8c684f 100644 --- a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js +++ b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js @@ -1,88 +1,19 @@ import Vue from 'vue'; import Translate from '~/vue_shared/translate'; import PromoteMilestoneModal from './components/promote_milestone_modal.vue'; -import eventHub from './event_hub'; Vue.use(Translate); export default () => { - const onRequestFinished = ({ milestoneUrl, successful }) => { - const button = document.querySelector( - `.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`, - ); - - if (!successful) { - button.removeAttribute('disabled'); - } - }; - - const onRequestStarted = milestoneUrl => { - const button = document.querySelector( - `.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`, - ); - button.setAttribute('disabled', ''); - eventHub.$once('promoteMilestoneModal.requestFinished', onRequestFinished); - }; - - const onDeleteButtonClick = event => { - const button = event.currentTarget; - const modalProps = { - milestoneTitle: button.dataset.milestoneTitle, - url: button.dataset.url, - groupName: button.dataset.groupName, - }; - eventHub.$once('promoteMilestoneModal.requestStarted', onRequestStarted); - eventHub.$emit('promoteMilestoneModal.props', modalProps); - }; - - const promoteMilestoneButtons = document.querySelectorAll('.js-promote-project-milestone-button'); - promoteMilestoneButtons.forEach(button => { - button.addEventListener('click', onDeleteButtonClick); - }); - - eventHub.$once('promoteMilestoneModal.mounted', () => { - promoteMilestoneButtons.forEach(button => { - button.removeAttribute('disabled'); - }); - }); - const promoteMilestoneModal = document.getElementById('promote-milestone-modal'); - let promoteMilestoneComponent; - - if (promoteMilestoneModal) { - promoteMilestoneComponent = new Vue({ - el: promoteMilestoneModal, - components: { - PromoteMilestoneModal, - }, - data() { - return { - modalProps: { - milestoneTitle: '', - groupName: '', - url: '', - }, - }; - }, - mounted() { - eventHub.$on('promoteMilestoneModal.props', this.setModalProps); - eventHub.$emit('promoteMilestoneModal.mounted'); - }, - beforeDestroy() { - eventHub.$off('promoteMilestoneModal.props', this.setModalProps); - }, - methods: { - setModalProps(modalProps) { - this.modalProps = modalProps; - }, - }, - render(createElement) { - return createElement('promote-milestone-modal', { - props: this.modalProps, - }); - }, - }); + if (!promoteMilestoneModal) { + return null; } - return promoteMilestoneComponent; + return new Vue({ + el: promoteMilestoneModal, + render(createElement) { + return createElement(PromoteMilestoneModal); + }, + }); }; diff --git a/app/assets/javascripts/pages/profiles/index.js b/app/assets/javascripts/pages/profiles/index.js index 883be18b336..d2b00d0ef45 100644 --- a/app/assets/javascripts/pages/profiles/index.js +++ b/app/assets/javascripts/pages/profiles/index.js @@ -4,7 +4,7 @@ import Profile from '~/profile/profile'; document.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line func-names - $(document).on('input.ssh_key', '#key_key', function() { + $(document).on('input.ssh_key', '#key_key', function () { const $title = $('#key_title'); const comment = $(this) .val() diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js index 60510eac384..b78f24ca2fb 100644 --- a/app/assets/javascripts/pages/profiles/show/index.js +++ b/app/assets/javascripts/pages/profiles/show/index.js @@ -14,7 +14,7 @@ document.addEventListener('DOMContentLoaded', () => { const statusEmojiField = document.getElementById('js-status-emoji-field'); const statusMessageField = document.getElementById('js-status-message-field'); - const toggleNoEmojiPlaceholder = isVisible => { + const toggleNoEmojiPlaceholder = (isVisible) => { const placeholderElement = document.getElementById('js-no-emoji-placeholder'); placeholderElement.classList.toggle('hidden', !isVisible); }; diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js index a96b88732b4..57c4ffd3933 100644 --- a/app/assets/javascripts/pages/projects/blob/show/index.js +++ b/app/assets/javascripts/pages/projects/blob/show/index.js @@ -41,7 +41,7 @@ document.addEventListener('DOMContentLoaded', () => { const { codeNavigationPath, blobPath, definitionPathPrefix } = codeNavEl.dataset; // eslint-disable-next-line promise/catch-or-return - import('~/code_navigation').then(m => + import('~/code_navigation').then((m) => m.default({ blobs: [{ path: blobPath, codeNavigationPath }], definitionPathPrefix, diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js index 0750f472341..5cfdb125e4f 100644 --- a/app/assets/javascripts/pages/projects/commit/show/index.js +++ b/app/assets/javascripts/pages/projects/commit/show/index.js @@ -14,6 +14,8 @@ import flash from '~/flash'; import { __ } from '~/locale'; import loadAwardsHandler from '~/awards_handler'; import { initCommitBoxInfo } from '~/projects/commit_box/info'; +import initRevertCommitTrigger from '~/projects/commit/init_revert_commit_trigger'; +import initRevertCommitModal from '~/projects/commit/init_revert_commit_modal'; const hasPerfBar = document.querySelector('.with-performance-bar'); const performanceHeight = hasPerfBar ? 35 : 0; @@ -45,3 +47,5 @@ if (filesContainer.length) { new Diff(); } loadAwardsHandler(); +initRevertCommitModal(); +initRevertCommitTrigger(); diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js index 7eeb0c852e5..5f1d3edc3ba 100644 --- a/app/assets/javascripts/pages/projects/edit/index.js +++ b/app/assets/javascripts/pages/projects/edit/index.js @@ -10,6 +10,7 @@ import initProjectPermissionsSettings from '../shared/permissions'; import initProjectDeleteButton from '~/projects/project_delete_button'; import UserCallout from '~/user_callout'; import initServiceDesk from '~/projects/settings_service_desk'; +import mountSearchSettings from './mount_search_settings'; document.addEventListener('DOMContentLoaded', () => { initFilePickers(); @@ -30,4 +31,6 @@ document.addEventListener('DOMContentLoaded', () => { '.js-general-settings-form, .js-mr-settings-form, .js-mr-approvals-form', ), ); + + mountSearchSettings(); }); diff --git a/app/assets/javascripts/pages/projects/edit/mount_search_settings.js b/app/assets/javascripts/pages/projects/edit/mount_search_settings.js new file mode 100644 index 00000000000..6c477dd7e80 --- /dev/null +++ b/app/assets/javascripts/pages/projects/edit/mount_search_settings.js @@ -0,0 +1,12 @@ +const mountSearchSettings = async () => { + const el = document.querySelector('.js-search-settings-app'); + + if (el) { + const { default: initSearch } = await import( + /* webpackChunkName: 'search_settings' */ '~/search_settings' + ); + initSearch({ el }); + } +}; + +export default mountSearchSettings; diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue index 6c0d20c55e9..a614443bcd9 100644 --- a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue +++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue @@ -31,7 +31,9 @@ export default { }, computed: { filteredNamespaces() { - return this.namespaces.filter(n => n.name.toLowerCase().includes(this.filter.toLowerCase())); + return this.namespaces.filter((n) => + n.name.toLowerCase().includes(this.filter.toLowerCase()), + ); }, }, @@ -43,7 +45,7 @@ export default { loadGroups() { axios .get(this.endpoint) - .then(response => { + .then((response) => { this.namespaces = response.data.namespaces; }) .catch(() => createFlash(__('There was a problem fetching groups.'))); diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js index 6cf36463bda..ea38b8e15a4 100644 --- a/app/assets/javascripts/pages/projects/graphs/charts/index.js +++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js @@ -5,7 +5,7 @@ import { __ } from '~/locale'; import CodeCoverage from '../components/code_coverage.vue'; import SeriesDataMixin from './series_data_mixin'; -const seriesDataToBarData = raw => Object.entries(raw).map(([name, data]) => ({ name, data })); +const seriesDataToBarData = (raw) => Object.entries(raw).map(([name, data]) => ({ name, data })); document.addEventListener('DOMContentLoaded', () => { waitForCSSLoaded(() => { @@ -43,7 +43,7 @@ document.addEventListener('DOMContentLoaded', () => { }, computed: { seriesData() { - return [{ name: 'full', data: this.chartData.map(d => [d.label, d.value]) }]; + return [{ name: 'full', data: this.chartData.map((d) => [d.label, d.value]) }]; }, }, render(h) { diff --git a/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue index 6dd50958fa4..3b5e764b712 100644 --- a/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue +++ b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue @@ -74,7 +74,7 @@ export default { ); }, formattedData() { - return this.sortedData.map(value => [dateFormat(value.date, 'mmm dd'), value.coverage]); + return this.sortedData.map((value) => [dateFormat(value.date, 'mmm dd'), value.coverage]); }, chartData() { return [ @@ -161,9 +161,7 @@ export default { <template #coveragePercentage> {{ coveragePercentage }} </template> - <template #percentSymbol> - % - </template> + <template #percentSymbol> % </template> </gl-sprintf> </template> </gl-area-chart> diff --git a/app/assets/javascripts/pages/projects/init_form.js b/app/assets/javascripts/pages/projects/init_form.js index 019efe077f7..9f20a3e4e46 100644 --- a/app/assets/javascripts/pages/projects/init_form.js +++ b/app/assets/javascripts/pages/projects/init_form.js @@ -1,7 +1,7 @@ import ZenMode from '~/zen_mode'; import GLForm from '~/gl_form'; -export default function($formEl) { +export default function ($formEl) { new ZenMode(); // eslint-disable-line no-new new GLForm($formEl); // eslint-disable-line no-new } diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js b/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js index fc0922d9112..ccb453a59ea 100644 --- a/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js +++ b/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js @@ -21,7 +21,7 @@ export default class FilteredSearchServiceDesk extends FilteredSearchManager { modifyUrlParams(paramsArray) { const supportBotParamPair = `${AUTHOR_PARAM_KEY}=${this.supportBotData.username}`; - const onlyValidParams = paramsArray.filter(param => param.indexOf(AUTHOR_PARAM_KEY) === -1); + const onlyValidParams = paramsArray.filter((param) => param.indexOf(AUTHOR_PARAM_KEY) === -1); // unshift ensures author param is always first token element onlyValidParams.unshift(supportBotParamPair); diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js index 614f8262e5b..7068574ecb8 100644 --- a/app/assets/javascripts/pages/projects/issues/show.js +++ b/app/assets/javascripts/pages/projects/issues/show.js @@ -16,7 +16,7 @@ import initInviteMemberModal from '~/invite_member/init_invite_member_modal'; import { IssuableType } from '~/issuable_show/constants'; -export default function() { +export default function () { const initialDataEl = document.getElementById('js-issuable-app'); const { issueType, ...issuableData } = parseIssuableData(initialDataEl); @@ -37,7 +37,7 @@ export default function() { initRelatedMergeRequestsApp(); import(/* webpackChunkName: 'design_management' */ '~/design_management') - .then(module => module.default()) + .then((module) => module.default()) .catch(() => {}); new ZenMode(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/jobs/index/index.js b/app/assets/javascripts/pages/projects/jobs/index/index.js index ae04d070e62..c343a37b292 100644 --- a/app/assets/javascripts/pages/projects/jobs/index/index.js +++ b/app/assets/javascripts/pages/projects/jobs/index/index.js @@ -5,7 +5,7 @@ import Tracking from '~/tracking'; document.addEventListener('DOMContentLoaded', () => { const remainingTimeElements = document.querySelectorAll('.js-remaining-time'); remainingTimeElements.forEach( - el => + (el) => new Vue({ ...GlCountdown, el, @@ -22,5 +22,5 @@ document.addEventListener('DOMContentLoaded', () => { } }; const buttons = document.querySelectorAll('.js-empty-state-button'); - buttons.forEach(button => button.addEventListener('click', trackButtonClick)); + buttons.forEach((button) => button.addEventListener('click', trackButtonClick)); }); diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue index 7b5e0f70b7b..8626fd18233 100644 --- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue +++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue @@ -58,14 +58,14 @@ export default { eventHub.$emit('promoteLabelModal.requestStarted', this.url); return axios .post(this.url, { params: { format: 'json' } }) - .then(response => { + .then((response) => { eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: true, }); visitUrl(response.data.url); }) - .catch(error => { + .catch((error) => { eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: false, diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js index ee129011f9a..4f5e5c8cceb 100644 --- a/app/assets/javascripts/pages/projects/labels/index/index.js +++ b/app/assets/javascripts/pages/projects/labels/index/index.js @@ -19,7 +19,7 @@ const initLabelIndex = () => { } }; - const onRequestStarted = labelUrl => { + const onRequestStarted = (labelUrl) => { const button = document.querySelector( `.js-promote-project-label-button[data-url="${labelUrl}"]`, ); @@ -46,7 +46,7 @@ const initLabelIndex = () => { eventHub.$on('promoteLabelModal.props', this.setModalProps); eventHub.$emit('promoteLabelModal.mounted'); - promoteLabelButtons.forEach(button => { + promoteLabelButtons.forEach((button) => { button.removeAttribute('disabled'); button.addEventListener('click', () => { this.$root.$emit('bv::show::modal', 'promote-label-modal'); @@ -77,5 +77,4 @@ const initLabelIndex = () => { }, }); }; - -document.addEventListener('DOMContentLoaded', initLabelIndex); +initLabelIndex(); diff --git a/app/assets/javascripts/pages/projects/labels/new/index.js b/app/assets/javascripts/pages/projects/labels/new/index.js index 83d6ac9fd14..2e8308fe084 100644 --- a/app/assets/javascripts/pages/projects/labels/new/index.js +++ b/app/assets/javascripts/pages/projects/labels/new/index.js @@ -1,3 +1,4 @@ import Labels from 'ee_else_ce/labels'; -document.addEventListener('DOMContentLoaded', () => new Labels()); +// eslint-disable-next-line no-new +new Labels(); diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js index 46f3f55a400..eb2692c7cb4 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js +++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js @@ -19,7 +19,7 @@ const updateCommitList = (url, $loadingIndicator, $commitList, params) => { }); }; -export default mrNewCompareNode => { +export default (mrNewCompareNode) => { const { sourceBranchUrl, targetBranchUrl } = mrNewCompareNode.dataset; initTargetProjectDropdown(); @@ -29,9 +29,7 @@ export default mrNewCompareNode => { $(mrNewCompareNode).find('.js-source-loading'), $(mrNewCompareNode).find('.mr_source_commit'), { - ref: $(mrNewCompareNode) - .find("input[name='merge_request[source_branch]']") - .val(), + ref: $(mrNewCompareNode).find("input[name='merge_request[source_branch]']").val(), }, ); const updateTargetBranchCommitList = () => @@ -43,12 +41,10 @@ export default mrNewCompareNode => { target_project_id: $(mrNewCompareNode) .find("input[name='merge_request[target_project_id]']") .val(), - ref: $(mrNewCompareNode) - .find("input[name='merge_request[target_branch]']") - .val(), + ref: $(mrNewCompareNode).find("input[name='merge_request[target_branch]']").val(), }, ); - initCompareAutocomplete('branches', $dropdown => { + initCompareAutocomplete('branches', ($dropdown) => { if ($dropdown.is('.js-target-branch')) { updateTargetBranchCommitList(); } else if ($dropdown.is('.js-source-branch')) { diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js index 0714fc21b17..1a0c5860991 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js @@ -1,3 +1,4 @@ +import Vue from 'vue'; import ZenMode from '~/zen_mode'; import initIssuableSidebar from '~/init_issuable_sidebar'; import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; @@ -7,8 +8,9 @@ import initSourcegraph from '~/sourcegraph'; import loadAwardsHandler from '~/awards_handler'; import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger'; import initInviteMemberModal from '~/invite_member/init_invite_member_modal'; +import StatusBox from '~/merge_request/components/status_box.vue'; -export default function() { +export default function () { new ZenMode(); // eslint-disable-line no-new initIssuableSidebar(); initPipelines(); @@ -18,4 +20,17 @@ export default function() { loadAwardsHandler(); initInviteMemberModal(); initInviteMemberTrigger(); + + const el = document.querySelector('.js-mr-status-box'); + // eslint-disable-next-line no-new + new Vue({ + el, + render(h) { + return h(StatusBox, { + props: { + initialState: el.dataset.state, + }, + }); + }, + }); } diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js index 19aeb1d1ecf..88f4db3ec08 100644 --- a/app/assets/javascripts/pages/projects/new/index.js +++ b/app/assets/javascripts/pages/projects/new/index.js @@ -10,7 +10,7 @@ document.addEventListener('DOMContentLoaded', () => { import( /* webpackChunkName: 'experiment_new_project_creation' */ '../../../projects/experiment_new_project_creation' ) - .then(m => { + .then((m) => { const el = document.querySelector('.js-experiment-new-project-creation'); if (!el) { @@ -20,6 +20,7 @@ document.addEventListener('DOMContentLoaded', () => { const config = { hasErrors: 'hasErrors' in el.dataset, isCiCdAvailable: 'isCiCdAvailable' in el.dataset, + newProjectGuidelines: el.dataset.newProjectGuidelines, }; m.default(el, config); }) diff --git a/app/assets/javascripts/pages/projects/pages_domains/form.js b/app/assets/javascripts/pages/projects/pages_domains/form.js index ae5368179b1..169530685ad 100644 --- a/app/assets/javascripts/pages/projects/pages_domains/form.js +++ b/app/assets/javascripts/pages/projects/pages_domains/form.js @@ -1,7 +1,7 @@ import setupToggleButtons from '~/toggle_buttons'; function updateVisibility(selector, isVisible) { - Array.from(document.querySelectorAll(selector)).forEach(el => { + Array.from(document.querySelectorAll(selector)).forEach((el) => { if (isVisible) { el.classList.remove('d-none'); } else { @@ -14,12 +14,12 @@ export default () => { const toggleContainer = document.querySelector('.js-auto-ssl-toggle-container'); if (toggleContainer) { - const onToggleButtonClicked = isAutoSslEnabled => { + const onToggleButtonClicked = (isAutoSslEnabled) => { updateVisibility('.js-shown-unless-auto-ssl', !isAutoSslEnabled); updateVisibility('.js-shown-if-auto-ssl', isAutoSslEnabled); - Array.from(document.querySelectorAll('.js-enabled-unless-auto-ssl')).forEach(el => { + Array.from(document.querySelectorAll('.js-enabled-unless-auto-ssl')).forEach((el) => { if (isAutoSslEnabled) { el.setAttribute('disabled', 'disabled'); } else { diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js index 90d2df50d5a..40730ec7e60 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js @@ -13,12 +13,12 @@ document.addEventListener('DOMContentLoaded', () => { // eslint-disable-next-line no-new new Vue({ el, - render(createElement) { - return createElement(PipelineSchedulesCallout); - }, provide: { docsUrl, illustrationUrl, }, + render(createElement) { + return createElement(PipelineSchedulesCallout); + }, }); }); diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue index 5ef1f959b2c..aa7414f3ae7 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue @@ -125,7 +125,7 @@ export default { :data-testid="option.value" > <gl-sprintf v-if="option.link" :message="option.text"> - <template #link="{content}"> + <template #link="{ content }"> <gl-link :href="option.link" target="_blank" class="gl-font-sm"> {{ content }} </gl-link> diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js index 4b203891640..6017cd653e4 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js @@ -15,19 +15,19 @@ export default class TargetBranchDropdown { data: this.formatBranchesList(), filterable: true, selectable: true, - toggleLabel: item => item.name, + toggleLabel: (item) => item.name, search: { fields: ['name'], }, - clicked: cfg => this.updateInputValue(cfg), - text: item => item.name, + clicked: (cfg) => this.updateInputValue(cfg), + text: (item) => item.name, }); this.setDropdownToggle(); } formatBranchesList() { - return this.$dropdown.data('data').map(val => ({ name: val })); + return this.$dropdown.data('data').map((val) => ({ name: val })); } setDropdownToggle() { diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js index 2a58e015ff1..16c4a6191b2 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js @@ -5,10 +5,10 @@ const defaults = { $inputEl: null, $dropdownEl: null, onSelectTimezone: null, - displayFormat: item => item.name, + displayFormat: (item) => item.name, }; -export const formatUtcOffset = offset => { +export const formatUtcOffset = (offset) => { const parsed = parseInt(offset, 10); if (Number.isNaN(parsed) || parsed === 0) { return `0`; @@ -17,11 +17,11 @@ export const formatUtcOffset = offset => { return `${prefix} ${Math.abs(offset / 3600)}`; }; -export const formatTimezone = item => `[UTC ${formatUtcOffset(item.offset)}] ${item.name}`; +export const formatTimezone = (item) => `[UTC ${formatUtcOffset(item.offset)}] ${item.name}`; export const findTimezoneByIdentifier = (tzList = [], identifier = null) => { if (tzList && tzList.length && identifier && identifier.length) { - return tzList.find(tz => tz.identifier === identifier) || null; + return tzList.find((tz) => tz.identifier === identifier) || null; } return null; }; @@ -52,8 +52,8 @@ export default class TimezoneDropdown { search: { fields: ['name'], }, - clicked: cfg => this.updateInputValue(cfg), - text: item => formatTimezone(item), + clicked: (cfg) => this.updateInputValue(cfg), + text: (item) => formatTimezone(item), }); this.setDropdownToggle(this.displayFormat(this.initialTimezone)); diff --git a/app/assets/javascripts/pages/projects/pipelines/charts/index.js b/app/assets/javascripts/pages/projects/pipelines/charts/index.js index d77b84a3b24..44d7555e639 100644 --- a/app/assets/javascripts/pages/projects/pipelines/charts/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/charts/index.js @@ -1,3 +1,3 @@ import initProjectPipelinesChartsApp from '~/projects/pipelines/charts/index'; -document.addEventListener('DOMContentLoaded', initProjectPipelinesChartsApp); +initProjectPipelinesChartsApp(); diff --git a/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js b/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js index 5fd3fce88aa..0c29f8817e7 100644 --- a/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js +++ b/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js @@ -6,7 +6,7 @@ export default () => { const fullMergeRequestList = document.querySelector('.js-full-mr-list'); if (mergeRequestListToggle) { - mergeRequestListToggle.addEventListener('click', e => { + mergeRequestListToggle.addEventListener('click', (e) => { e.preventDefault(); truncatedMergeRequestList.classList.toggle('hide'); fullMergeRequestList.classList.toggle('hide'); diff --git a/app/assets/javascripts/pages/projects/pipelines/new/index.js b/app/assets/javascripts/pages/projects/pipelines/new/index.js index d5563143f0c..08c31f2b3c6 100644 --- a/app/assets/javascripts/pages/projects/pipelines/new/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/new/index.js @@ -3,17 +3,15 @@ import NewBranchForm from '~/new_branch_form'; import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list'; import initNewPipeline from '~/pipeline_new/index'; -document.addEventListener('DOMContentLoaded', () => { - const el = document.getElementById('js-new-pipeline'); +const el = document.getElementById('js-new-pipeline'); - if (el) { - initNewPipeline(); - } else { - new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new +if (el) { + initNewPipeline(); +} else { + new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new - setupNativeFormVariableList({ - container: $('.js-ci-variable-list-section'), - formField: 'variables_attributes', - }); - } -}); + setupNativeFormVariableList({ + container: $('.js-ci-variable-list-section'), + formField: 'variables_attributes', + }); +} diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index 8c7aa04a0b6..ef6953db83b 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -18,34 +18,26 @@ export default class Project { // Ref switcher if (document.querySelector('.js-project-refs-dropdown')) { Project.initRefSwitcher(); - $('.project-refs-select').on('change', function() { - return $(this) - .parents('form') - .trigger('submit'); + $('.project-refs-select').on('change', function () { + return $(this).parents('form').trigger('submit'); }); } - $('.hide-no-ssh-message').on('click', function(e) { + $('.hide-no-ssh-message').on('click', function (e) { Cookies.set('hide_no_ssh_message', 'false'); - $(this) - .parents('.no-ssh-key-message') - .remove(); + $(this).parents('.no-ssh-key-message').remove(); return e.preventDefault(); }); - $('.hide-no-password-message').on('click', function(e) { + $('.hide-no-password-message').on('click', function (e) { Cookies.set('hide_no_password_message', 'false'); - $(this) - .parents('.no-password-message') - .remove(); + $(this).parents('.no-password-message').remove(); return e.preventDefault(); }); - $('.hide-auto-devops-implicitly-enabled-banner').on('click', function(e) { + $('.hide-auto-devops-implicitly-enabled-banner').on('click', function (e) { const projectId = $(this).data('project-id'); const cookieKey = `hide_auto_devops_implicitly_enabled_banner_${projectId}`; Cookies.set(cookieKey, 'false'); - $(this) - .parents('.auto-devops-implicitly-enabled-banner') - .remove(); + $(this).parents('.auto-devops-implicitly-enabled-banner').remove(); return e.preventDefault(); }); @@ -54,7 +46,7 @@ export default class Project { static projectSelectDropdown() { projectSelect(); - $('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val())); + $('.project-item-select').on('click', (e) => Project.changeProject($(e.currentTarget).val())); } static changeProject(url) { @@ -67,7 +59,7 @@ export default class Project { refLink.href = '#'; - return $('.js-project-refs-dropdown').each(function() { + return $('.js-project-refs-dropdown').each(function () { const $dropdown = $(this); const selected = $dropdown.data('selected'); const fieldName = $dropdown.data('fieldName'); @@ -132,7 +124,7 @@ export default class Project { if (loc.includes('/-/')) { const refs = this.fullData.Branches.concat(this.fullData.Tags); - const currentRef = refs.find(ref => loc.indexOf(ref) > -1); + const currentRef = refs.find((ref) => loc.indexOf(ref) > -1); if (currentRef) { const targetPath = loc.split(currentRef)[1].slice(1); selectedUrl.searchParams.set('path', targetPath); diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js index e146592e134..3e0a48ee6a2 100644 --- a/app/assets/javascripts/pages/projects/project_members/index.js +++ b/app/assets/javascripts/pages/projects/project_members/index.js @@ -1,9 +1,11 @@ import Vue from 'vue'; -import Members from 'ee_else_ce/members'; +import Members from '~/members'; import memberExpirationDate from '~/member_expiration_date'; import UsersSelect from '~/users_select'; import groupsSelect from '~/groups_select'; import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; +import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; +import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; function mountRemoveMemberModal() { const el = document.querySelector('.js-remove-member-modal'); @@ -24,6 +26,8 @@ document.addEventListener('DOMContentLoaded', () => { memberExpirationDate(); memberExpirationDate('.js-access-expiration-date-groups'); mountRemoveMemberModal(); + initInviteMembersModal(); + initInviteMembersTrigger(); new Members(); // eslint-disable-line no-new new UsersSelect(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js index 83bec0092cb..1321155b7ec 100644 --- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js @@ -5,6 +5,7 @@ import initVariableList from '~/ci_variable_list'; import initDeployFreeze from '~/deploy_freeze'; import initSettingsPipelinesTriggers from '~/ci_settings_pipeline_triggers'; import initSharedRunnersToggle from '~/projects/settings/mount_shared_runners_toggle'; +import initArtifactsSettings from '~/artifacts_settings'; document.addEventListener('DOMContentLoaded', () => { // Initialize expandable settings panels @@ -23,7 +24,7 @@ document.addEventListener('DOMContentLoaded', () => { // hide extra auto devops settings based checkbox state const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings'); const instanceDefaultBadge = document.querySelector('.js-instance-default-badge'); - document.querySelector('.js-toggle-extra-settings').addEventListener('click', event => { + document.querySelector('.js-toggle-extra-settings').addEventListener('click', (event) => { const { target } = event; if (instanceDefaultBadge) instanceDefaultBadge.style.display = 'none'; autoDevOpsExtraSettings.classList.toggle('hidden', !target.checked); @@ -33,6 +34,7 @@ document.addEventListener('DOMContentLoaded', () => { initDeployFreeze(); initSettingsPipelinesTriggers(); + initArtifactsSettings(); if (gon?.features?.vueifySharedRunnersToggle) { initSharedRunnersToggle(); diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue index 242c58c4981..eee666bea05 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue @@ -33,6 +33,11 @@ export default { required: false, default: false, }, + showToggle: { + type: Boolean, + required: false, + default: true, + }, }, computed: { featureEnabled() { @@ -74,6 +79,7 @@ export default { > <input v-if="name" :name="name" :value="value" type="hidden" /> <project-feature-toggle + v-if="showToggle" class="gl-flex-grow-0 gl-mr-3" :value="featureEnabled" :disabled-input="disabledInput" diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index be197a50775..4af476fbd68 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -12,6 +12,7 @@ import { featureAccessLevelMembers, featureAccessLevelEveryone, featureAccessLevel, + featureAccessLevelNone, } from '../constants'; import { toggleHiddenClassBySelector } from '../external'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -140,6 +141,7 @@ export default { metricsDashboardAccessLevel: featureAccessLevel.PROJECT_MEMBERS, analyticsAccessLevel: featureAccessLevel.EVERYONE, requirementsAccessLevel: featureAccessLevel.EVERYONE, + operationsAccessLevel: featureAccessLevel.EVERYONE, containerRegistryEnabled: true, lfsEnabled: true, requestAccessEnabled: true, @@ -167,6 +169,14 @@ export default { ); }, + operationsFeatureAccessLevelOptions() { + if (!this.operationsEnabled) return [featureAccessLevelNone]; + + return this.featureAccessLevelOptions.filter( + ([value]) => value <= this.operationsAccessLevel, + ); + }, + pagesFeatureAccessLevelOptions() { const options = [featureAccessLevelMembers]; @@ -186,8 +196,12 @@ export default { return options; }, - metricsOptionsDropdownEnabled() { - return this.featureAccessLevelOptions.length < 2; + metricsOptionsDropdownDisabled() { + return this.operationsFeatureAccessLevelOptions.length < 2 || !this.operationsEnabled; + }, + + operationsEnabled() { + return this.operationsAccessLevel > featureAccessLevel.NOT_ENABLED; }, repositoryEnabled() { @@ -250,6 +264,10 @@ export default { featureAccessLevel.PROJECT_MEMBERS, this.requirementsAccessLevel, ); + this.operationsAccessLevel = Math.min( + featureAccessLevel.PROJECT_MEMBERS, + this.operationsAccessLevel, + ); if (this.pagesAccessLevel === featureAccessLevel.EVERYONE) { // When from Internal->Private narrow access for only members this.pagesAccessLevel = featureAccessLevel.PROJECT_MEMBERS; @@ -277,6 +295,8 @@ export default { this.metricsDashboardAccessLevel = featureAccessLevel.EVERYONE; if (this.requirementsAccessLevel === featureAccessLevel.PROJECT_MEMBERS) this.requirementsAccessLevel = featureAccessLevel.EVERYONE; + if (this.operationsAccessLevel === featureAccessLevel.PROJECT_MEMBERS) + this.operationsAccessLevel = featureAccessLevel.EVERYONE; this.highlightChanges(); } @@ -334,18 +354,21 @@ export default { <option :value="visibilityOptions.PRIVATE" :disabled="!visibilityAllowed(visibilityOptions.PRIVATE)" - >{{ s__('ProjectSettings|Private') }}</option > + {{ s__('ProjectSettings|Private') }} + </option> <option :value="visibilityOptions.INTERNAL" :disabled="!visibilityAllowed(visibilityOptions.INTERNAL)" - >{{ s__('ProjectSettings|Internal') }}</option > + {{ s__('ProjectSettings|Internal') }} + </option> <option :value="visibilityOptions.PUBLIC" :disabled="!visibilityAllowed(visibilityOptions.PUBLIC)" - >{{ s__('ProjectSettings|Public') }}</option > + {{ s__('ProjectSettings|Public') }} + </option> </select> <gl-icon name="chevron-down" @@ -354,6 +377,11 @@ export default { /> </div> </div> + <span v-if="!visibilityAllowed(visibilityLevel)" class="form-text text-muted">{{ + s__( + 'ProjectSettings|Visibility options for this fork are limited by the current visibility of the source project.', + ) + }}</span> <span class="form-text text-muted">{{ visibilityLevelDescription }}</span> <label v-if="visibilityLevel !== visibilityOptions.PRIVATE" class="gl-line-height-28"> <input @@ -562,41 +590,34 @@ export default { /> </project-setting-row> <project-setting-row - ref="metrics-visibility-settings" - :label="__('Metrics Dashboard')" - :help-text=" - s__( - 'ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics', - ) - " + ref="operations-settings" + :label="s__('ProjectSettings|Operations')" + :help-text="s__('ProjectSettings|Environments, logs, cluster management, and more')" > - <div class="project-feature-controls gl-display-flex gl-align-items-center gl-my-3 gl-mx-0"> - <div class="select-wrapper gl-flex-fill-1"> - <select - v-model="metricsDashboardAccessLevel" - :disabled="metricsOptionsDropdownEnabled" - name="project[project_feature_attributes][metrics_dashboard_access_level]" - class="form-control project-repo-select select-control" - > - <option - :value="featureAccessLevelMembers[0]" - :disabled="!visibilityAllowed(visibilityOptions.INTERNAL)" - >{{ featureAccessLevelMembers[1] }}</option - > - <option - :value="featureAccessLevelEveryone[0]" - :disabled="!visibilityAllowed(visibilityOptions.PUBLIC)" - >{{ featureAccessLevelEveryone[1] }}</option - > - </select> - <gl-icon - name="chevron-down" - data-hidden="true" - class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-500" - /> - </div> - </div> + <project-feature-setting + v-model="operationsAccessLevel" + :options="featureAccessLevelOptions" + name="project[project_feature_attributes][operations_access_level]" + /> </project-setting-row> + <div class="project-feature-setting-group gl-pl-7 gl-sm-pl-5"> + <project-setting-row + ref="metrics-visibility-settings" + :label="__('Metrics Dashboard')" + :help-text=" + s__( + 'ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics', + ) + " + > + <project-feature-setting + v-model="metricsDashboardAccessLevel" + :show-toggle="false" + :options="operationsFeatureAccessLevelOptions" + name="project[project_feature_attributes][metrics_dashboard_access_level]" + /> + </project-setting-row> + </div> </div> <project-setting-row v-if="canDisableEmails" ref="email-settings" class="mb-3"> <label class="js-emails-disabled"> diff --git a/app/assets/javascripts/pages/projects/shared/permissions/external.js b/app/assets/javascripts/pages/projects/shared/permissions/external.js index 460af4a2111..49f6ebcc20f 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/external.js +++ b/app/assets/javascripts/pages/projects/shared/permissions/external.js @@ -14,5 +14,5 @@ export function toggleHiddenClassBySelector(selector, hidden) { if (!selectorCache[selector]) { selectorCache[selector] = document.querySelectorAll(selector); } - selectorCache[selector].forEach(elm => toggleHiddenClass(elm, hidden)); + selectorCache[selector].forEach((elm) => toggleHiddenClass(elm, hidden)); } diff --git a/app/assets/javascripts/pages/projects/shared/permissions/index.js b/app/assets/javascripts/pages/projects/shared/permissions/index.js index dbde8dda634..d7bae44e96e 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/index.js +++ b/app/assets/javascripts/pages/projects/shared/permissions/index.js @@ -8,6 +8,6 @@ export default function initProjectPermissionsSettings() { return new Vue({ el: mountPoint, - render: createElement => createElement(settingsPanel, { props: { ...componentProps } }), + render: (createElement) => createElement(settingsPanel, { props: { ...componentProps } }), }); } diff --git a/app/assets/javascripts/pages/projects/tags/index/index.js b/app/assets/javascripts/pages/projects/tags/index/index.js index ec56fa3e075..96e52850936 100644 --- a/app/assets/javascripts/pages/projects/tags/index/index.js +++ b/app/assets/javascripts/pages/projects/tags/index/index.js @@ -2,11 +2,8 @@ import { initRemoveTag } from '../remove_tag'; document.addEventListener('DOMContentLoaded', () => { initRemoveTag({ - onDelete: path => { - document - .querySelector(`[data-path="${path}"]`) - .closest('.js-tag-list') - .remove(); + onDelete: (path) => { + document.querySelector(`[data-path="${path}"]`).closest('.js-tag-list').remove(); }, }); }); diff --git a/app/assets/javascripts/pages/search/show/refresh_counts.js b/app/assets/javascripts/pages/search/show/refresh_counts.js index fa75ee6075d..f3f6312cb7c 100644 --- a/app/assets/javascripts/pages/search/show/refresh_counts.js +++ b/app/assets/javascripts/pages/search/show/refresh_counts.js @@ -11,7 +11,7 @@ function refreshCount(el) { return axios .get(url) .then(({ data }) => showCount(el, data.count)) - .catch(e => { + .catch((e) => { // eslint-disable-next-line no-console console.error(`Failed to fetch search count from '${url}'.`, e); }); diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js index b411b637f36..cbef5ab1bbc 100644 --- a/app/assets/javascripts/pages/search/show/search.js +++ b/app/assets/javascripts/pages/search/show/search.js @@ -16,16 +16,12 @@ export default class Search { } eventListeners() { - $(document) - .off('keyup', this.searchInput) - .on('keyup', this.searchInput, this.searchKeyUp); + $(document).off('keyup', this.searchInput).on('keyup', this.searchInput, this.searchKeyUp); $(document) .off('click', this.searchClear) .on('click', this.searchClear, this.clearSearchField.bind(this)); - $('a.js-search-clear') - .off('click', this.clearSearchFilter) - .on('click', this.clearSearchFilter); + $('a.js-search-clear').off('click', this.clearSearchFilter).on('click', this.clearSearchFilter); } static submitSearch() { @@ -42,10 +38,7 @@ export default class Search { } clearSearchField() { - return $(this.searchInput) - .val('') - .trigger('keyup') - .focus(); + return $(this.searchInput).val('').trigger('keyup').focus(); } // We need to manually follow the link on the anchors diff --git a/app/assets/javascripts/pages/sessions/new/length_validator.js b/app/assets/javascripts/pages/sessions/new/length_validator.js index 92482c81f3c..17acad10bc1 100644 --- a/app/assets/javascripts/pages/sessions/new/length_validator.js +++ b/app/assets/javascripts/pages/sessions/new/length_validator.js @@ -9,7 +9,7 @@ export default class LengthValidator extends InputValidator { const container = opts.container || ''; const validateLengthElements = document.querySelectorAll(`${container} .js-validate-length`); - validateLengthElements.forEach(element => + validateLengthElements.forEach((element) => element.addEventListener('input', this.eventHandler.bind(this)), ); } diff --git a/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js b/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js index 1d47a9aed47..70e5e336e78 100644 --- a/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js +++ b/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js @@ -13,7 +13,7 @@ export default function preserveUrlFragment(fragment = '') { // Append the fragment to all sign-in/sign-up form actions so it is preserved when the user is // eventually redirected back to the originally requested URL. const forms = document.querySelectorAll('#signin-container .tab-content form'); - Array.prototype.forEach.call(forms, form => { + Array.prototype.forEach.call(forms, (form) => { const actionWithFragment = setUrlFragment(form.getAttribute('action'), `#${normalFragment}`); form.setAttribute('action', actionWithFragment); }); @@ -21,7 +21,7 @@ export default function preserveUrlFragment(fragment = '') { // Append a redirect_fragment query param to all oauth provider links. The redirect_fragment // query param will be available in the omniauth callback upon successful authentication const oauthForms = document.querySelectorAll('#signin-container .omniauth-container form'); - Array.prototype.forEach.call(oauthForms, oauthForm => { + Array.prototype.forEach.call(oauthForms, (oauthForm) => { const newHref = mergeUrlParams( { redirect_fragment: normalFragment }, oauthForm.getAttribute('action'), diff --git a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js index 2b8f1e8b0ef..1e7c29aefaa 100644 --- a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js +++ b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js @@ -20,7 +20,7 @@ export default class SigninTabsMemoizer { bootstrap() { const tabs = document.querySelectorAll(this.tabSelector); if (tabs.length > 0) { - tabs[0].addEventListener('click', e => { + tabs[0].addEventListener('click', (e) => { if (e.target && e.target.nodeName === 'A') { const anchorName = e.target.getAttribute('href'); this.saveData(anchorName); diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js index 62f6e3fb84f..f3b0948a40d 100644 --- a/app/assets/javascripts/pages/sessions/new/username_validator.js +++ b/app/assets/javascripts/pages/sessions/new/username_validator.js @@ -20,11 +20,11 @@ export default class UsernameValidator extends InputValidator { const container = opts.container || ''; const validateLengthElements = document.querySelectorAll(`${container} .js-validate-username`); - this.debounceValidateInput = debounce(inputDomElement => { + this.debounceValidateInput = debounce((inputDomElement) => { UsernameValidator.validateUsernameInput(inputDomElement); }, debounceTimeoutDuration); - validateLengthElements.forEach(element => + validateLengthElements.forEach((element) => element.addEventListener('input', this.eventHandler.bind(this)), ); } @@ -42,7 +42,7 @@ export default class UsernameValidator extends InputValidator { if (inputDomElement.checkValidity() && username.length > 1) { UsernameValidator.setMessageVisibility(inputDomElement, pendingMessageSelector); UsernameValidator.fetchUsernameAvailability(username) - .then(usernameTaken => { + .then((usernameTaken) => { UsernameValidator.setInputState(inputDomElement, !usernameTaken); UsernameValidator.setMessageVisibility(inputDomElement, pendingMessageSelector, false); UsernameValidator.setMessageVisibility( diff --git a/app/assets/javascripts/pages/shared/mount_badge_settings.js b/app/assets/javascripts/pages/shared/mount_badge_settings.js index 1397c0834ff..aeb9f2fb8d3 100644 --- a/app/assets/javascripts/pages/shared/mount_badge_settings.js +++ b/app/assets/javascripts/pages/shared/mount_badge_settings.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import BadgeSettings from '~/badges/components/badge_settings.vue'; import store from '~/badges/store'; -export default kind => { +export default (kind) => { const badgeSettingsElement = document.getElementById('badge-settings'); store.dispatch('loadBadges', { diff --git a/app/assets/javascripts/pages/shared/wikis/wikis.js b/app/assets/javascripts/pages/shared/wikis/wikis.js index fe9caba351e..4b4d2f7d238 100644 --- a/app/assets/javascripts/pages/shared/wikis/wikis.js +++ b/app/assets/javascripts/pages/shared/wikis/wikis.js @@ -20,12 +20,13 @@ export default class Wikis { const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle'); for (let i = 0; i < sidebarToggles.length; i += 1) { - sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e)); + sidebarToggles[i].addEventListener('click', (e) => this.handleToggleSidebar(e)); } this.isNewWikiPage = Boolean(document.querySelector('.js-new-wiki-page')); this.editTitleInput = document.querySelector('form.wiki-form #wiki_title'); this.commitMessageInput = document.querySelector('form.wiki-form #wiki_message'); + this.submitButton = document.querySelector('.js-wiki-btn-submit'); this.commitMessageI18n = this.isNewWikiPage ? s__('WikiPageCreate|Create %{pageTitle}') : s__('WikiPageEdit|Update %{pageTitle}'); @@ -35,7 +36,7 @@ export default class Wikis { if (this.editTitleInput.value) this.setWikiCommitMessage(this.editTitleInput.value); // Set the commit message as the page title is changed - this.editTitleInput.addEventListener('keyup', e => this.handleWikiTitleChange(e)); + this.editTitleInput.addEventListener('keyup', (e) => this.handleWikiTitleChange(e)); } window.addEventListener('resize', () => this.renderSidebar()); @@ -45,18 +46,16 @@ export default class Wikis { const linkExample = document.querySelector('.js-markup-link-example'); if (changeFormatSelect) { - changeFormatSelect.addEventListener('change', e => { + changeFormatSelect.addEventListener('change', (e) => { linkExample.innerHTML = MARKDOWN_LINK_TEXT[e.target.value]; }); } - const wikiTextarea = document.querySelector('form.wiki-form #wiki_content'); + this.wikiTextarea = document.querySelector('form.wiki-form #wiki_content'); const wikiForm = document.querySelector('form.wiki-form'); - if (wikiTextarea) { - wikiTextarea.addEventListener('input', () => { - window.onbeforeunload = () => ''; - }); + if (this.wikiTextarea) { + this.wikiTextarea.addEventListener('input', () => this.handleWikiContentChange()); wikiForm.addEventListener('submit', () => { window.onbeforeunload = null; @@ -65,12 +64,29 @@ export default class Wikis { Wikis.trackPageView(); Wikis.showToasts(); + + this.updateSubmitButton(); + } + + handleWikiContentChange() { + this.updateSubmitButton(); + + window.onbeforeunload = () => ''; } handleWikiTitleChange(e) { + this.updateSubmitButton(); this.setWikiCommitMessage(e.target.value); } + updateSubmitButton() { + if (!this.wikiTextarea) return; + + const isEnabled = Boolean(this.wikiTextarea.value.trim() && this.editTitleInput.value.trim()); + if (isEnabled) this.submitButton.removeAttribute('disabled'); + else this.submitButton.setAttribute('disabled', 'true'); + } + setWikiCommitMessage(rawTitle) { let title = rawTitle; @@ -121,6 +137,6 @@ export default class Wikis { static showToasts() { const toasts = document.querySelectorAll('.js-toast-message'); - toasts.forEach(toast => showToast(toast.dataset.message)); + toasts.forEach((toast) => showToast(toast.dataset.message)); } } diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js index 54666af540e..149e666256b 100644 --- a/app/assets/javascripts/pages/users/activity_calendar.js +++ b/app/assets/javascripts/pages/users/activity_calendar.js @@ -42,11 +42,7 @@ function formatTooltipText({ date, count }) { return `${contribText}<br />${dateDayName} ${dateText}`; } -const initColorKey = () => - d3 - .scaleLinear() - .range(['#acd5f2', '#254e77']) - .domain([0, 3]); +const initColorKey = () => d3.scaleLinear().range(['#acd5f2', '#254e77']).domain([0, 3]); export default class ActivityCalendar { constructor( @@ -177,17 +173,17 @@ export default class ActivityCalendar { return `translate(${this.daySizeWithSpace * i + 1 + this.daySizeWithSpace}, 18)`; }) .selectAll('rect') - .data(stamp => stamp) + .data((stamp) => stamp) .enter() .append('rect') .attr('x', '0') - .attr('y', stamp => this.dayYPos(stamp.day)) + .attr('y', (stamp) => this.dayYPos(stamp.day)) .attr('width', this.daySize) .attr('height', this.daySize) - .attr('fill', stamp => + .attr('fill', (stamp) => stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed', ) - .attr('title', stamp => formatTooltipText(stamp)) + .attr('title', (stamp) => formatTooltipText(stamp)) .attr('class', 'user-contrib-cell has-tooltip') .attr('data-html', true) .attr('data-container', 'body') @@ -230,8 +226,8 @@ export default class ActivityCalendar { .append('text') .attr('text-anchor', 'middle') .attr('x', 8) - .attr('y', day => day.y) - .text(day => day.text) + .attr('y', (day) => day.y) + .text((day) => day.text) .attr('class', 'user-contrib-text'); } @@ -243,10 +239,10 @@ export default class ActivityCalendar { .data(this.months) .enter() .append('text') - .attr('x', date => date.x) + .attr('x', (date) => date.x) .attr('y', 10) .attr('class', 'user-contrib-text') - .text(date => this.monthNames[date.month]); + .text((date) => this.monthNames[date.month]); } renderKey() { @@ -276,7 +272,7 @@ export default class ActivityCalendar { .attr('height', this.daySize) .attr('x', (color, i) => this.daySizeWithSpace * i) .attr('y', 0) - .attr('fill', color => color) + .attr('fill', (color) => color) .attr('class', 'has-tooltip') .attr('title', (color, i) => keyValues[i]) .attr('data-container', 'body') @@ -291,10 +287,7 @@ export default class ActivityCalendar { this.colorKey(2), this.colorKey(3), ]; - return d3 - .scaleThreshold() - .domain([0, 10, 20, 30]) - .range(colorRange); + return d3.scaleThreshold().domain([0, 10, 20, 30]).range(colorRange); } clickDay(stamp) { diff --git a/app/assets/javascripts/pages/users/index.js b/app/assets/javascripts/pages/users/index.js index 8adbc2a8168..b22287a0093 100644 --- a/app/assets/javascripts/pages/users/index.js +++ b/app/assets/javascripts/pages/users/index.js @@ -8,12 +8,10 @@ function initUserProfile(action) { new UserTabs({ parentEl: '.user-profile', action }); // hide project limit message - $('.hide-project-limit-message').on('click', e => { + $('.hide-project-limit-message').on('click', (e) => { e.preventDefault(); Cookies.set('hide_project_limit_message', 'false'); - $(this) - .parents('.project-limit-message') - .remove(); + $(this).parents('.project-limit-message').remove(); }); } diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js index 2485853afc7..7c88aa53e4b 100644 --- a/app/assets/javascripts/pages/users/user_tabs.js +++ b/app/assets/javascripts/pages/users/user_tabs.js @@ -100,8 +100,8 @@ export default class UserTabs { bindEvents() { this.$parentEl .off('shown.bs.tab', '.nav-links a[data-toggle="tab"]') - .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event)) - .on('click', '.gl-pagination a', event => this.changeProjectsPage(event)); + .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', (event) => this.tabShown(event)) + .on('click', '.gl-pagination a', (event) => this.changeProjectsPage(event)); window.addEventListener('resize', () => this.onResize()); } @@ -212,17 +212,19 @@ export default class UserTabs { const calendarPath = $calendarWrap.data('calendarPath'); AjaxCache.retrieve(calendarPath) - .then(data => UserTabs.renderActivityCalendar(data, $calendarWrap)) + .then((data) => UserTabs.renderActivityCalendar(data, $calendarWrap)) .catch(() => { const cWrap = $calendarWrap[0]; cWrap.querySelector('.spinner').classList.add('invisible'); cWrap.querySelector('.user-calendar-error').classList.remove('invisible'); - cWrap.querySelector('.user-calendar-error .js-retry-load').addEventListener('click', e => { - e.preventDefault(); - cWrap.querySelector('.user-calendar-error').classList.add('invisible'); - cWrap.querySelector('.spinner').classList.remove('invisible'); - this.loadActivityCalendar(); - }); + cWrap + .querySelector('.user-calendar-error .js-retry-load') + .addEventListener('click', (e) => { + e.preventDefault(); + cWrap.querySelector('.user-calendar-error').classList.add('invisible'); + cWrap.querySelector('.spinner').classList.remove('invisible'); + this.loadActivityCalendar(); + }); }); } diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue index bbbd9789dc9..c8a04eb72c4 100644 --- a/app/assets/javascripts/pdf/index.vue +++ b/app/assets/javascripts/pdf/index.vue @@ -40,11 +40,11 @@ export default { cMapPacked: true, }) .promise.then(this.renderPages) - .then(pages => { + .then((pages) => { this.pages = pages; this.$emit('pdflabload'); }) - .catch(error => { + .catch((error) => { this.$emit('pdflaberror', error); }); }, diff --git a/app/assets/javascripts/pdf/page/index.vue b/app/assets/javascripts/pdf/page/index.vue index 843c50cf9bc..ae0a7f0298b 100644 --- a/app/assets/javascripts/pdf/page/index.vue +++ b/app/assets/javascripts/pdf/page/index.vue @@ -39,7 +39,7 @@ export default { .promise.then(() => { this.rendering = false; }) - .catch(error => { + .catch((error) => { this.$emit('pdflaberror', error); }); }, diff --git a/app/assets/javascripts/performance/utils.js b/app/assets/javascripts/performance/utils.js index 1c87ee2086e..0001ef54244 100644 --- a/app/assets/javascripts/performance/utils.js +++ b/app/assets/javascripts/performance/utils.js @@ -3,7 +3,7 @@ export const performanceMarkAndMeasure = ({ mark, measures = [] } = {}) => { if (mark && !performance.getEntriesByName(mark).length) { performance.mark(mark); } - measures.forEach(measure => { + measures.forEach((measure) => { performance.measure(measure.name, measure.start, measure.end); }); }); diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue index 90e14d8325f..930c5e50511 100644 --- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue +++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue @@ -76,12 +76,12 @@ export default { this.openedBacktraces = [...this.openedBacktraces, toggledIndex]; } else { this.openedBacktraces = this.openedBacktraces.filter( - openedIndex => openedIndex !== toggledIndex, + (openedIndex) => openedIndex !== toggledIndex, ); } }, itemHasOpenedBacktrace(toggledIndex) { - return this.openedBacktraces.find(openedIndex => openedIndex === toggledIndex) >= 0; + return this.openedBacktraces.find((openedIndex) => openedIndex === toggledIndex) >= 0; }, }, }; @@ -141,7 +141,7 @@ export default { </template> </table> - <template #footer> + <template #modal-footer> <div></div> </template> </gl-modal> diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue index 5a9d3a6d313..5666e038f02 100644 --- a/app/assets/javascripts/performance_bar/components/request_selector.vue +++ b/app/assets/javascripts/performance_bar/components/request_selector.vue @@ -25,7 +25,7 @@ export default { }, computed: { requestsWithWarnings() { - return this.requests.filter(request => request.hasWarnings); + return this.requests.filter((request) => request.hasWarnings); }, warningMessage() { return n__( diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js index e0b7f2190ca..0d5c294ea56 100644 --- a/app/assets/javascripts/performance_bar/index.js +++ b/app/assets/javascripts/performance_bar/index.js @@ -10,7 +10,10 @@ import initPerformanceBarLog from './performance_bar_log'; Vue.use(Translate); -const initPerformanceBar = el => { +const initPerformanceBar = (el) => { + if (!el) { + return undefined; + } const performanceBarData = el.dataset; return new Vue({ @@ -55,7 +58,7 @@ const initPerformanceBar = el => { this.store.addRequest(requestId, requestUrl); PerformanceBarService.fetchRequestDetails(this.peekUrl, requestId) - .then(res => { + .then((res) => { this.store.addRequestDetails(requestId, res.data); if (this.requestId === requestId) this.collectFrontendPerformanceMetrics(); @@ -84,7 +87,7 @@ const initPerformanceBar = el => { if ('PerformanceObserver' in window) { // We start observing for more incoming timings - const observer = new PerformanceObserver(list => { + const observer = new PerformanceObserver((list) => { newEntries = newEntries.concat(list.getEntries().map(this.transformResourceEntry)); this.updateFrontendPerformanceMetrics(durationString, newEntries); }); @@ -126,25 +129,7 @@ const initPerformanceBar = el => { }); }; -let loadedPeekBar = false; -function loadBar() { - const jsPeek = document.querySelector('#js-peek'); - if (!loadedPeekBar && jsPeek) { - loadedPeekBar = true; - initPerformanceBar(jsPeek); - } -} - -// If js-peek is not loaded when this script is executed, this call will do nothing -// If this is the case, then it will loadBar on DOMContentLoaded. We would prefer it -// to be initialized before the DOMContetLoaded event in order to pick up all the -// requests sent from the page. -loadBar(); - -document.addEventListener('DOMContentLoaded', () => { - loadBar(); -}); - +initPerformanceBar(document.querySelector('#js-peek')); initPerformanceBarLog(); export default initPerformanceBar; diff --git a/app/assets/javascripts/performance_bar/performance_bar_log.js b/app/assets/javascripts/performance_bar/performance_bar_log.js index 3ba7ff1c221..c61b0cb32e8 100644 --- a/app/assets/javascripts/performance_bar/performance_bar_log.js +++ b/app/assets/javascripts/performance_bar/performance_bar_log.js @@ -3,7 +3,7 @@ import { getCLS, getFID, getLCP } from 'web-vitals'; import { PERFORMANCE_TYPE_MARK, PERFORMANCE_TYPE_MEASURE } from '~/performance/constants'; const initVitalsLog = () => { - const reportVital = data => { + const reportVital = (data) => { console.log(`${String.fromCodePoint(0x1f4c8)} ${data.name} : `, data); }; @@ -18,9 +18,9 @@ const initVitalsLog = () => { }; const logUserTimingMetrics = () => { - const metricsProcessor = list => { + const metricsProcessor = (list) => { const entries = list.getEntries(); - entries.forEach(entry => { + entries.forEach((entry) => { const { name, entryType, startTime, duration } = entry; const typeMapper = { [PERFORMANCE_TYPE_MARK]: String.fromCodePoint(0x1f3af), diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js index 3c8303d102e..38255b3a37d 100644 --- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js +++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js @@ -9,7 +9,7 @@ export default class PerformanceBarService { } static registerInterceptor(peekUrl, callback) { - PerformanceBarService.interceptor = response => { + PerformanceBarService.interceptor = (response) => { const [fireCallback, requestId, requestUrl] = PerformanceBarService.callbackParams( response, peekUrl, diff --git a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js index 8c88851f039..9d12d228d35 100644 --- a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js +++ b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js @@ -20,7 +20,7 @@ export default class PerformanceBarStore { } findRequest(requestId) { - return this.requests.find(request => request.id === requestId); + return this.requests.find((request) => request.id === requestId); } addRequestDetails(requestId, requestDetails) { @@ -43,13 +43,13 @@ export default class PerformanceBarStore { } requestsWithDetails() { - return this.requests.filter(request => request.details); + return this.requests.filter((request) => request.details); } canTrackRequest(requestUrl) { return ( requestUrl.endsWith('/api/graphql') || - this.requests.filter(request => request.url === requestUrl).length < 2 + this.requests.filter((request) => request.url === requestUrl).length < 2 ); } diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js index eded64127b6..db42966d159 100644 --- a/app/assets/javascripts/persistent_user_callout.js +++ b/app/assets/javascripts/persistent_user_callout.js @@ -28,10 +28,10 @@ export default class PersistentUserCallout { } handleCloseButtonCallout(closeButton) { - closeButton.addEventListener('click', event => this.dismiss(event)); + closeButton.addEventListener('click', (event) => this.dismiss(event)); if (this.deferLinks) { - this.container.addEventListener('click', event => { + this.container.addEventListener('click', (event) => { const isDeferredLink = event.target.classList.contains(DEFERRED_LINK_CLASS); if (isDeferredLink) { const { href, target } = event.target; @@ -43,7 +43,7 @@ export default class PersistentUserCallout { } handleFollowLinkCallout(followLink) { - followLink.addEventListener('click', event => this.registerCalloutWithLink(event)); + followLink.addEventListener('click', (event) => this.registerCalloutWithLink(event)); } dismiss(event, deferredLinkOptions = null) { diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js index d4857a19ff7..c177fe25985 100644 --- a/app/assets/javascripts/persistent_user_callouts.js +++ b/app/assets/javascripts/persistent_user_callouts.js @@ -11,7 +11,7 @@ const PERSISTENT_USER_CALLOUTS = [ ]; const initCallouts = () => { - PERSISTENT_USER_CALLOUTS.forEach(calloutContainer => + PERSISTENT_USER_CALLOUTS.forEach((calloutContainer) => PersistentUserCallout.factory(document.querySelector(calloutContainer)), ); }; diff --git a/app/assets/javascripts/pipeline_editor/components/info/validation_segment.vue b/app/assets/javascripts/pipeline_editor/components/info/validation_segment.vue new file mode 100644 index 00000000000..22f378c571a --- /dev/null +++ b/app/assets/javascripts/pipeline_editor/components/info/validation_segment.vue @@ -0,0 +1,84 @@ +<script> +import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui'; +import { __, s__, sprintf } from '~/locale'; +import { CI_CONFIG_STATUS_VALID } from '../../constants'; +import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; + +export const i18n = { + learnMore: __('Learn more'), + loading: s__('Pipelines|Validating GitLab CI configurationā¦'), + invalid: s__('Pipelines|This GitLab CI configuration is invalid.'), + invalidWithReason: s__('Pipelines|This GitLab CI configuration is invalid: %{reason}.'), + valid: s__('Pipelines|This GitLab CI configuration is valid.'), +}; + +export default { + i18n, + components: { + GlIcon, + GlLink, + GlLoadingIcon, + TooltipOnTruncate, + }, + inject: { + ymlHelpPagePath: { + default: '', + }, + }, + props: { + ciConfig: { + type: Object, + required: false, + default: () => ({}), + }, + loading: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + isValid() { + return this.ciConfig?.status === CI_CONFIG_STATUS_VALID; + }, + icon() { + if (this.isValid) { + return 'check'; + } + return 'warning-solid'; + }, + message() { + if (this.isValid) { + return this.$options.i18n.valid; + } + + // Only display first error as a reason + const [reason] = this.ciConfig?.errors || []; + if (reason) { + return sprintf(this.$options.i18n.invalidWithReason, { reason }, false); + } + return this.$options.i18n.invalid; + }, + }, +}; +</script> + +<template> + <div> + <template v-if="loading"> + <gl-loading-icon inline /> + {{ $options.i18n.loading }} + </template> + + <span v-else class="gl-display-inline-flex gl-white-space-nowrap gl-max-w-full"> + <tooltip-on-truncate :title="message" class="gl-text-truncate"> + <gl-icon :name="icon" /> <span data-testid="validationMsg">{{ message }}</span> + </tooltip-on-truncate> + <span class="gl-flex-shrink-0 gl-pl-2"> + <gl-link data-testid="learnMoreLink" :href="ymlHelpPagePath"> + {{ $options.i18n.learnMore }} + </gl-link> + </span> + </span> + </div> +</template> diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint.vue b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint.vue new file mode 100644 index 00000000000..b27ab9a39d3 --- /dev/null +++ b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint.vue @@ -0,0 +1,53 @@ +<script> +import { flatten } from 'lodash'; +import { CI_CONFIG_STATUS_VALID } from '../../constants'; +import CiLintResults from './ci_lint_results.vue'; + +export default { + components: { + CiLintResults, + }, + inject: { + lintHelpPagePath: { + default: '', + }, + }, + props: { + ciConfig: { + type: Object, + required: true, + }, + }, + computed: { + isValid() { + return this.ciConfig?.status === CI_CONFIG_STATUS_VALID; + }, + stages() { + return this.ciConfig?.stages || []; + }, + jobs() { + const groupedJobs = this.stages.reduce((acc, { groups, name: stageName }) => { + return acc.concat( + groups.map(({ jobs }) => { + return jobs.map((job) => ({ + stage: stageName, + ...job, + })); + }), + ); + }, []); + + return flatten(groupedJobs); + }, + }, +}; +</script> + +<template> + <ci-lint-results + :valid="isValid" + :jobs="jobs" + :errors="ciConfig.errors" + :lint-help-page-path="lintHelpPagePath" + /> +</template> diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue index 0d1c214c5b1..58a96c3f725 100644 --- a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue +++ b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue @@ -10,11 +10,11 @@ const thBorderColor = 'gl-border-gray-100!'; export default { correct: { variant: 'success', - text: __('syntax is correct.'), + text: __('Syntax is correct.'), }, incorrect: { variant: 'danger', - text: __('syntax is incorrect.'), + text: __('Syntax is incorrect.'), }, includesText: __( 'CI configuration validated, including all configuration added with the %{codeStart}includes%{codeEnd} keyword. %{link}', @@ -48,19 +48,23 @@ export default { }, jobs: { type: Array, - required: true, + required: false, + default: () => [], }, errors: { type: Array, - required: true, + required: false, + default: () => [], }, warnings: { type: Array, - required: true, + required: false, + default: () => [], }, dryRun: { type: Boolean, - required: true, + required: false, + default: false, }, lintHelpPagePath: { type: String, @@ -99,7 +103,7 @@ export default { data-testid="ci-lint-status" >{{ status.text }} <gl-sprintf :message="$options.includesText"> - <template #code="{content}"> + <template #code="{ content }"> <code> {{ content }} </code> diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue index 4929c3206df..ef2be2a5fba 100644 --- a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue +++ b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue @@ -14,7 +14,7 @@ export default { }, computed: { tagList() { - return this.item.tagList.join(', '); + return this.item.tags?.join(', '); }, onlyPolicy() { return this.item.only ? this.item.only.refs.join(', ') : this.item.only; @@ -26,15 +26,15 @@ export default { return { beforeScript: { show: !isEmpty(this.item.beforeScript), - content: this.item.beforeScript.join('\n'), + content: this.item.beforeScript?.join('\n'), }, script: { show: !isEmpty(this.item.script), - content: this.item.script.join('\n'), + content: this.item.script?.join('\n'), }, afterScript: { show: !isEmpty(this.item.afterScript), - content: this.item.afterScript.join('\n'), + content: this.item.afterScript?.join('\n'), }, }; }, @@ -43,35 +43,43 @@ export default { </script> <template> - <div> - <pre v-if="scripts.beforeScript.show" data-testid="ci-lint-before-script">{{ - scripts.beforeScript.content - }}</pre> - <pre v-if="scripts.script.show" data-testid="ci-lint-script">{{ scripts.script.content }}</pre> - <pre v-if="scripts.afterScript.show" data-testid="ci-lint-after-script">{{ - scripts.afterScript.content + <div data-testid="ci-lint-value"> + <pre + v-if="scripts.beforeScript.show" + class="gl-white-space-pre-wrap" + data-testid="ci-lint-before-script" + >{{ scripts.beforeScript.content }}</pre + > + <pre v-if="scripts.script.show" class="gl-white-space-pre-wrap" data-testid="ci-lint-script">{{ + scripts.script.content }}</pre> + <pre + v-if="scripts.afterScript.show" + class="gl-white-space-pre-wrap" + data-testid="ci-lint-after-script" + >{{ scripts.afterScript.content }}</pre + > <ul class="gl-list-style-none gl-pl-0 gl-mb-0"> - <li> + <li v-if="tagList"> <b>{{ __('Tag list:') }}</b> {{ tagList }} </li> <div v-if="!dryRun" data-testid="ci-lint-only-except"> - <li> + <li v-if="onlyPolicy"> <b>{{ __('Only policy:') }}</b> {{ onlyPolicy }} </li> - <li> + <li v-if="exceptPolicy"> <b>{{ __('Except policy:') }}</b> {{ exceptPolicy }} </li> </div> - <li> + <li v-if="item.environment"> <b>{{ __('Environment:') }}</b> {{ item.environment }} </li> - <li> + <li v-if="item.when"> <b>{{ __('When:') }}</b> {{ item.when }} <b v-if="item.allowFailure">{{ __('Allowed to fail') }}</b> diff --git a/app/assets/javascripts/pipeline_editor/components/text_editor.vue b/app/assets/javascripts/pipeline_editor/components/text_editor.vue index 22f2a32c9ac..b8d49d77ea9 100644 --- a/app/assets/javascripts/pipeline_editor/components/text_editor.vue +++ b/app/assets/javascripts/pipeline_editor/components/text_editor.vue @@ -1,14 +1,46 @@ <script> import EditorLite from '~/vue_shared/components/editor_lite.vue'; +import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext'; export default { components: { EditorLite, }, + inject: ['projectPath', 'projectNamespace'], + inheritAttrs: false, + props: { + ciConfigPath: { + type: String, + required: true, + }, + commitSha: { + type: String, + required: false, + default: null, + }, + }, + methods: { + onEditorReady() { + const editorInstance = this.$refs.editor.getEditor(); + + editorInstance.use(new CiSchemaExtension()); + editorInstance.registerCiSchema({ + projectPath: this.projectPath, + projectNamespace: this.projectNamespace, + ref: this.commitSha, + }); + }, + }, }; </script> <template> <div class="gl-border-solid gl-border-gray-100 gl-border-1"> - <editor-lite file-name="*.yml" v-bind="$attrs" v-on="$listeners" /> + <editor-lite + ref="editor" + :file-name="ciConfigPath" + v-bind="$attrs" + @editor-ready="onEditorReady" + v-on="$listeners" + /> </div> </template> diff --git a/app/assets/javascripts/pipeline_editor/components/ui/editor_tab.vue b/app/assets/javascripts/pipeline_editor/components/ui/editor_tab.vue new file mode 100644 index 00000000000..b0acd3ca2ee --- /dev/null +++ b/app/assets/javascripts/pipeline_editor/components/ui/editor_tab.vue @@ -0,0 +1,68 @@ +<script> +import { GlTab } from '@gitlab/ui'; + +/** + * Wrapper of <gl-tab> to optionally lazily render this tab's content + * when its shown **without dismounting after its hidden**. + * + * Usage: + * + * API is the same as <gl-tab>, for example: + * + * <gl-tabs> + * <editor-tab title="Tab 1" :lazy="true"> + * lazily mounted content (gets mounted if this is first tab) + * </editor-tab> + * <editor-tab title="Tab 2" :lazy="true"> + * lazily mounted content + * </editor-tab> + * <editor-tab title="Tab 3"> + * eagerly mounted content + * </editor-tab> + * </gl-tabs> + * + * Once the tab is selected it is permanently set as "not-lazy" + * so it's contents are not dismounted. + * + * lazy is "false" by default, as in <gl-tab>. + */ + +export default { + components: { + GlTab, + // Use a small renderless component to know when the tab content mounts because: + // - gl-tab always gets mounted, even if lazy is `true`. See: + // https://github.com/bootstrap-vue/bootstrap-vue/blob/dev/src/components/tabs/tab.js#L180 + // - we cannot listen to events on <slot /> + MountSpy: { + render: () => null, + }, + }, + inheritAttrs: false, + props: { + lazy: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + isLazy: this.lazy, + }; + }, + methods: { + onContentMounted() { + // When a child is first mounted make the entire tab + // permanently mounted by setting 'lazy' to false. + this.isLazy = false; + }, + }, +}; +</script> +<template> + <gl-tab :lazy="isLazy" v-bind="$attrs" v-on="$listeners"> + <slot v-for="slot in Object.keys($slots)" :slot="slot" :name="slot"></slot> + <mount-spy @hook:mounted="onContentMounted" /> + </gl-tab> +</template> diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql index 11bca42fd69..0c58749a8b2 100644 --- a/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql +++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql @@ -19,7 +19,7 @@ mutation commitCIFileMutation( } ) { commit { - id + sha } errors } diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/lint_ci.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/lint_ci.mutation.graphql index 496036f690f..5091d63111f 100644 --- a/app/assets/javascripts/pipeline_editor/graphql/mutations/lint_ci.mutation.graphql +++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/lint_ci.mutation.graphql @@ -15,7 +15,7 @@ mutation lintCI($endpoint: String, $content: String, $dry: Boolean) { } afterScript stage - tagList + tags when } } diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql index d65d9892260..dfddb29701d 100644 --- a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql +++ b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql @@ -1,7 +1,7 @@ -#import "~/pipelines/graphql/queries/pipeline_stages_connection.fragment.graphql" +#import "~/pipelines/graphql/fragments/pipeline_stages_connection.fragment.graphql" -query getCiConfigData($content: String!) { - ciConfig(content: $content) { +query getCiConfigData($projectPath: ID!, $content: String!) { + ciConfig(projectPath: $projectPath, content: $content) { errors status stages { diff --git a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js index c1cdb5eb2ee..81e75c32846 100644 --- a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js +++ b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js @@ -18,7 +18,7 @@ export const resolvers = { valid: data.valid, errors: data.errors, warnings: data.warnings, - jobs: data.jobs.map(job => { + jobs: data.jobs.map((job) => { const only = job.only ? { refs: job.only.refs, __typename: 'CiLintJobOnlyPolicy' } : null; return { @@ -27,7 +27,7 @@ export const resolvers = { beforeScript: job.before_script, script: job.script, afterScript: job.after_script, - tagList: job.tag_list, + tags: job.tag_list, environment: job.environment, when: job.when, allowFailure: job.allow_failure, diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js index 8268a907a29..583ba555080 100644 --- a/app/assets/javascripts/pipeline_editor/index.js +++ b/app/assets/javascripts/pipeline_editor/index.js @@ -14,7 +14,20 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => { return null; } - const { ciConfigPath, commitId, defaultBranch, newMergeRequestPath, projectPath } = el?.dataset; + const { + // props + ciConfigPath, + commitSha, + defaultBranch, + newMergeRequestPath, + + // `provide/inject` data + lintHelpPagePath, + projectFullPath, + projectPath, + projectNamespace, + ymlHelpPagePath, + } = el?.dataset; Vue.use(VueApollo); @@ -25,14 +38,20 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => { return new Vue({ el, apolloProvider, + provide: { + lintHelpPagePath, + projectFullPath, + projectPath, + projectNamespace, + ymlHelpPagePath, + }, render(h) { return h(PipelineEditorApp, { props: { ciConfigPath, - commitId, + commitSha, defaultBranch, newMergeRequestPath, - projectPath, }, }); }, diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue index 96dc782964b..21993e2120a 100644 --- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue +++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue @@ -1,12 +1,16 @@ <script> -import { GlAlert, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui'; +import { GlAlert, GlLoadingIcon, GlTabs, GlTab } from '@gitlab/ui'; import { __, s__, sprintf } from '~/locale'; -import { mergeUrlParams, redirectTo, refreshCurrentPage } from '~/lib/utils/url_utility'; +import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import httpStatusCodes from '~/lib/utils/http_status'; import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue'; +import CiLint from './components/lint/ci_lint.vue'; import CommitForm from './components/commit/commit_form.vue'; +import EditorTab from './components/ui/editor_tab.vue'; import TextEditor from './components/text_editor.vue'; +import ValidationSegment from './components/info/validation_segment.vue'; import commitCiFileMutation from './graphql/mutations/commit_ci_file.mutation.graphql'; import getBlobContent from './graphql/queries/blob_content.graphql'; @@ -17,33 +21,33 @@ const MR_SOURCE_BRANCH = 'merge_request[source_branch]'; const MR_TARGET_BRANCH = 'merge_request[target_branch]'; const COMMIT_FAILURE = 'COMMIT_FAILURE'; +const COMMIT_SUCCESS = 'COMMIT_SUCCESS'; const DEFAULT_FAILURE = 'DEFAULT_FAILURE'; const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE'; -const LOAD_FAILURE_NO_REF = 'LOAD_FAILURE_NO_REF'; const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN'; export default { components: { + CiLint, CommitForm, + EditorTab, GlAlert, GlLoadingIcon, - GlTab, GlTabs, + GlTab, PipelineGraph, TextEditor, + ValidationSegment, }, mixins: [glFeatureFlagsMixin()], + inject: ['projectFullPath'], props: { - projectPath: { - type: String, - required: true, - }, defaultBranch: { type: String, required: false, default: null, }, - commitId: { + commitSha: { type: String, required: false, default: null, @@ -62,12 +66,15 @@ export default { ciConfigData: {}, content: '', contentModel: '', - currentTabIndex: 0, - editorIsReady: false, - failureType: null, - failureReasons: [], + lastCommitSha: this.commitSha, isSaving: false, + + // Success and failure state + failureType: null, showFailureAlert: false, + failureReasons: [], + successType: null, + showSuccessAlert: false, }; }, apollo: { @@ -75,7 +82,7 @@ export default { query: getBlobContent, variables() { return { - projectPath: this.projectPath, + projectPath: this.projectFullPath, path: this.ciConfigPath, ref: this.defaultBranch, }; @@ -98,15 +105,16 @@ export default { }, variables() { return { + projectPath: this.projectFullPath, content: this.contentModel, }; }, update(data) { - const { ciConfigData } = data || {}; - const stageNodes = ciConfigData?.stages?.nodes || []; + const { ciConfig } = data || {}; + const stageNodes = ciConfig?.stages?.nodes || []; const stages = unwrapStagesWithNeeds(stageNodes); - return { ...ciConfigData, stages }; + return { ...ciConfig, stages }; }, error() { this.reportFailure(LOAD_FAILURE_UNKNOWN); @@ -117,40 +125,48 @@ export default { isBlobContentLoading() { return this.$apollo.queries.content.loading; }, - isVisualizationTabLoading() { - return this.$apollo.queries.ciConfigData.loading; + isBlobContentError() { + return this.failureType === LOAD_FAILURE_NO_FILE; }, - isVisualizeTabActive() { - return this.currentTabIndex === 1; + isCiConfigDataLoading() { + return this.$apollo.queries.ciConfigData.loading; }, defaultCommitMessage() { return sprintf(this.$options.i18n.defaultCommitMessage, { sourcePath: this.ciConfigPath }); }, - failure() { - switch (this.failureType) { - case LOAD_FAILURE_NO_REF: + success() { + switch (this.successType) { + case COMMIT_SUCCESS: return { - text: this.$options.errorTexts[LOAD_FAILURE_NO_REF], - variant: 'danger', + text: this.$options.alertTexts[COMMIT_SUCCESS], + variant: 'info', }; + default: + return null; + } + }, + failure() { + switch (this.failureType) { case LOAD_FAILURE_NO_FILE: return { - text: this.$options.errorTexts[LOAD_FAILURE_NO_FILE], + text: sprintf(this.$options.alertTexts[LOAD_FAILURE_NO_FILE], { + filePath: this.ciConfigPath, + }), variant: 'danger', }; case LOAD_FAILURE_UNKNOWN: return { - text: this.$options.errorTexts[LOAD_FAILURE_UNKNOWN], + text: this.$options.alertTexts[LOAD_FAILURE_UNKNOWN], variant: 'danger', }; case COMMIT_FAILURE: return { - text: this.$options.errorTexts[COMMIT_FAILURE], + text: this.$options.alertTexts[COMMIT_FAILURE], variant: 'danger', }; default: return { - text: this.$options.errorTexts[DEFAULT_FAILURE], + text: this.$options.alertTexts[DEFAULT_FAILURE], variant: 'danger', }; } @@ -160,30 +176,34 @@ export default { defaultCommitMessage: __('Update %{sourcePath} file'), tabEdit: s__('Pipelines|Write pipeline configuration'), tabGraph: s__('Pipelines|Visualize'), + tabLint: s__('Pipelines|Lint'), }, - errorTexts: { - [LOAD_FAILURE_NO_REF]: s__( - 'Pipelines|Repository does not have a default branch, please set one.', + alertTexts: { + [COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'), + [COMMIT_SUCCESS]: __('Your changes have been successfully committed.'), + [DEFAULT_FAILURE]: __('Something went wrong on our end.'), + [LOAD_FAILURE_NO_FILE]: s__( + 'Pipelines|There is no %{filePath} file in this repository, please add one and visit the Pipeline Editor again.', ), - [LOAD_FAILURE_NO_FILE]: s__('Pipelines|No CI file found in this repository, please add one.'), [LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'), - [COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'), }, methods: { handleBlobContentError(error = {}) { const { networkError } = error; const { response } = networkError; - if (response?.status === 404) { - // 404 for missing CI file + // 404 for missing CI file + // 400 for blank projects with no repository + if ( + response?.status === httpStatusCodes.NOT_FOUND || + response?.status === httpStatusCodes.BAD_REQUEST + ) { this.reportFailure(LOAD_FAILURE_NO_FILE); - } else if (response?.status === 400) { - // 400 for a missing ref when no default branch is set - this.reportFailure(LOAD_FAILURE_NO_REF); } else { this.reportFailure(LOAD_FAILURE_UNKNOWN); } }, + dismissFailure() { this.showFailureAlert = false; }, @@ -192,6 +212,14 @@ export default { this.failureType = type; this.failureReasons = reasons; }, + dismissSuccess() { + this.showSuccessAlert = false; + }, + reportSuccess(type) { + this.showSuccessAlert = true; + this.successType = type; + }, + redirectToNewMergeRequest(sourceBranch) { const url = mergeUrlParams( { @@ -209,18 +237,18 @@ export default { try { const { data: { - commitCreate: { errors }, + commitCreate: { errors, commit }, }, } = await this.$apollo.mutate({ mutation: commitCiFileMutation, variables: { - projectPath: this.projectPath, + projectPath: this.projectFullPath, branch, startBranch: this.defaultBranch, message, filePath: this.ciConfigPath, content: this.contentModel, - lastCommitId: this.commitId, + lastCommitId: this.lastCommitSha, }, }); @@ -232,8 +260,10 @@ export default { if (openMergeRequest) { this.redirectToNewMergeRequest(branch); } else { - // Refresh the page to ensure commit is updated - refreshCurrentPage(); + this.reportSuccess(COMMIT_SUCCESS); + + // Update latest commit + this.lastCommitSha = commit.sha; } } catch (error) { this.reportFailure(COMMIT_FAILURE, [error?.message]); @@ -251,6 +281,14 @@ export default { <template> <div class="gl-mt-4"> <gl-alert + v-if="showSuccessAlert" + :variant="success.variant" + :dismissible="true" + @dismiss="dismissSuccess" + > + {{ success.text }} + </gl-alert> + <gl-alert v-if="showFailureAlert" :variant="failure.variant" :dismissible="true" @@ -261,25 +299,39 @@ export default { <li v-for="reason in failureReasons" :key="reason">{{ reason }}</li> </ul> </gl-alert> - <div class="gl-mt-4"> - <gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" /> - <div v-else class="file-editor gl-mb-3"> - <gl-tabs v-model="currentTabIndex"> - <!-- editor should be mounted when its tab is visible, so the container has a size --> - <gl-tab :title="$options.i18n.tabEdit" :lazy="!editorIsReady"> - <!-- editor should be mounted only once, when the tab is displayed --> - <text-editor v-model="contentModel" @editor-ready="editorIsReady = true" /> - </gl-tab> + <gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" /> + <div v-else-if="!isBlobContentError" class="gl-mt-4"> + <div class="file-editor gl-mb-3"> + <div class="info-well gl-display-none gl-display-sm-block"> + <validation-segment + class="well-segment" + :loading="isCiConfigDataLoading" + :ci-config="ciConfigData" + /> + </div> + <gl-tabs> + <editor-tab :lazy="true" :title="$options.i18n.tabEdit"> + <text-editor + v-model="contentModel" + :ci-config-path="ciConfigPath" + :commit-sha="lastCommitSha" + /> + </editor-tab> <gl-tab v-if="glFeatures.ciConfigVisualizationTab" + :lazy="true" :title="$options.i18n.tabGraph" - :lazy="!isVisualizeTabActive" data-testid="visualization-tab" > - <gl-loading-icon v-if="isVisualizationTabLoading" size="lg" class="gl-m-3" /> + <gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" /> <pipeline-graph v-else :pipeline-data="ciConfigData" /> </gl-tab> + + <editor-tab :title="$options.i18n.tabLint"> + <gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" /> + <ci-lint v-else :ci-config="ciConfigData" /> + </editor-tab> </gl-tabs> </div> <commit-form diff --git a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue index f2d68054e80..70c5713b216 100644 --- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue +++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue @@ -16,6 +16,7 @@ import { GlSearchBoxByType, GlSprintf, GlLoadingIcon, + GlSafeHtmlDirective as SafeHtml, } from '@gitlab/ui'; import * as Sentry from '~/sentry/wrapper'; import { s__, __, n__ } from '~/locale'; @@ -34,7 +35,7 @@ export default { 'Pipeline|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used by default.', ), formElementClasses: 'gl-mr-3 gl-mb-3 gl-flex-basis-quarter gl-flex-shrink-0 gl-flex-grow-0', - errorTitle: __('The form contains the following error:'), + errorTitle: __('Pipeline cannot be run.'), warningTitle: __('The form contains the following warning:'), maxWarningsSummary: __('%{total} warnings found: showing first %{warningsDisplayed}'), components: { @@ -53,6 +54,7 @@ export default { GlSprintf, GlLoadingIcon, }, + directives: { SafeHtml }, props: { pipelinesPath: { type: String, @@ -121,12 +123,12 @@ export default { return this.searchTerm.toLowerCase(); }, filteredBranches() { - return this.branches.filter(branch => + return this.branches.filter((branch) => branch.shortName.toLowerCase().includes(this.lowerCasedSearchTerm), ); }, filteredTags() { - return this.tags.filter(tag => + return this.tags.filter((tag) => tag.shortName.toLowerCase().includes(this.lowerCasedSearchTerm), ); }, @@ -187,7 +189,7 @@ export default { setVariable(refValue, type, key, value) { const { variables } = this.form[refValue]; - const variable = variables.find(v => v.key === key); + const variable = variables.find((v) => v.key === key); if (variable) { variable.type = type; variable.value = value; @@ -270,11 +272,11 @@ export default { stop(data); } }) - .catch(error => { + .catch((error) => { stop(error); }); }, CONFIG_VARIABLES_TIMEOUT) - .then(data => { + .then((data) => { const params = {}; const descriptions = {}; @@ -287,7 +289,7 @@ export default { return { params, descriptions }; }) - .catch(error => { + .catch((error) => { this.isLoading = false; Sentry.captureException(error); @@ -314,7 +316,7 @@ export default { .then(({ data }) => { redirectTo(`${this.pipelinesPath}/${data.id}`); }) - .catch(err => { + .catch((err) => { const { errors, warnings, total_warnings: totalWarnings } = err.response.data; const [error] = errors; this.error = error; @@ -335,8 +337,9 @@ export default { variant="danger" class="gl-mb-4" data-testid="run-pipeline-error-alert" - >{{ error }}</gl-alert > + <span v-safe-html="error"></span> + </gl-alert> <gl-alert v-if="shouldShowWarning" :title="$options.warningTitle" @@ -365,7 +368,7 @@ export default { </p> </details> </gl-alert> - <gl-form-group :label="s__('Pipeline|Run for')"> + <gl-form-group :label="s__('Pipeline|Run for branch name or tag')"> <gl-dropdown :text="refShortName" block> <gl-search-box-by-type v-model.trim="searchTerm" :placeholder="__('Search refs')" /> <gl-dropdown-section-header>{{ __('Branches') }}</gl-dropdown-section-header> @@ -391,12 +394,6 @@ export default { {{ tag.shortName }} </gl-dropdown-item> </gl-dropdown> - - <template #description> - <div> - {{ s__('Pipeline|Existing branch name or tag') }} - </div></template - > </gl-form-group> <gl-loading-icon v-if="isLoading" class="gl-mb-5" size="lg" /> diff --git a/app/assets/javascripts/pipeline_new/utils/format_refs.js b/app/assets/javascripts/pipeline_new/utils/format_refs.js index e217cd25413..f0fbc5ed7b6 100644 --- a/app/assets/javascripts/pipeline_new/utils/format_refs.js +++ b/app/assets/javascripts/pipeline_new/utils/format_refs.js @@ -3,7 +3,7 @@ import { BRANCH_REF_TYPE, TAG_REF_TYPE } from '../constants'; export default (refs, type) => { let fullName; - return refs.map(ref => { + return refs.map((ref) => { if (type === BRANCH_REF_TYPE) { fullName = `refs/heads/${ref}`; } else if (type === TAG_REF_TYPE) { diff --git a/app/assets/javascripts/pipelines.js b/app/assets/javascripts/pipelines.js index 07abe714367..3b4e8d0e019 100644 --- a/app/assets/javascripts/pipelines.js +++ b/app/assets/javascripts/pipelines.js @@ -1,5 +1,5 @@ import LinkedTabs from './lib/utils/bootstrap_linked_tabs'; -import { setCiStatusFavicon } from './lib/utils/common_utils'; +import { setCiStatusFavicon } from './lib/utils/favicon_ci'; export default class Pipelines { constructor(options = {}) { diff --git a/app/assets/javascripts/pipelines/components/dag/dag.vue b/app/assets/javascripts/pipelines/components/dag/dag.vue index 85171263f08..2482af2c7f0 100644 --- a/app/assets/javascripts/pipelines/components/dag/dag.vue +++ b/app/assets/javascripts/pipelines/components/dag/dag.vue @@ -56,15 +56,15 @@ export default { const unwrappedGroups = stages .map(({ name, groups: { nodes: groups } }) => { - return groups.map(group => { + return groups.map((group) => { return { category: name, ...group }; }); }) .flat(2); - const nodes = unwrappedGroups.map(group => { + const nodes = unwrappedGroups.map((group) => { const jobs = group.jobs.nodes.map(({ name, needs }) => { - return { name, needs: needs.nodes.map(need => need.name) }; + return { name, needs: needs.nodes.map((need) => need.name) }; }); return { ...group, jobs }; diff --git a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue index 42d1debcddf..5ba0604fa01 100644 --- a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue +++ b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue @@ -173,7 +173,7 @@ export default { createClip(link) { return link .append('clipPath') - .attr('id', d => { + .attr('id', (d) => { return this.createAndAssignId(d, 'clipId', 'dag-clip'); }) .append('path') @@ -183,7 +183,7 @@ export default { createGradient(link) { const gradient = link .append('linearGradient') - .attr('id', d => { + .attr('id', (d) => { return this.createAndAssignId(d, 'gradId', 'dag-grad'); }) .attr('gradientUnits', 'userSpaceOnUse') @@ -251,7 +251,7 @@ export default { .data(linksData) .enter() .append('g') - .attr('id', d => { + .attr('id', (d) => { return this.createAndAssignId(d, 'uid', LINK_SELECTOR); }) .classed( @@ -273,10 +273,10 @@ export default { `${NODE_SELECTOR} gl-transition-property-stroke ${this.$options.viewOptions.hoverFadeClasses}`, true, ) - .attr('id', d => { + .attr('id', (d) => { return this.createAndAssignId(d, 'uid', NODE_SELECTOR); }) - .attr('stroke', d => { + .attr('stroke', (d) => { const color = this.color(d); /* eslint-disable-next-line no-param-reassign */ d.color = color; @@ -284,10 +284,10 @@ export default { }) .attr('stroke-width', nodeWidth) .attr('stroke-linecap', 'round') - .attr('x1', d => Math.floor((d.x1 + d.x0) / 2)) - .attr('x2', d => Math.floor((d.x1 + d.x0) / 2)) - .attr('y1', d => d.y0 + 4) - .attr('y2', d => d.y1 - 4); + .attr('x1', (d) => Math.floor((d.x1 + d.x0) / 2)) + .attr('x2', (d) => Math.floor((d.x1 + d.x0) / 2)) + .attr('y1', (d) => d.y0 + 4) + .attr('y2', (d) => d.y1 - 4); }, initColors() { diff --git a/app/assets/javascripts/pipelines/components/dag/drawing_utils.js b/app/assets/javascripts/pipelines/components/dag/drawing_utils.js index d56addc473f..3cd09d57ffb 100644 --- a/app/assets/javascripts/pipelines/components/dag/drawing_utils.js +++ b/app/assets/javascripts/pipelines/components/dag/drawing_utils.js @@ -92,8 +92,8 @@ export const createSankey = ({ ]); return ({ nodes, links }) => sankeyGenerator({ - nodes: nodes.map(d => ({ ...d })), - links: links.map(d => ({ ...d })), + nodes: nodes.map((d) => ({ ...d })), + links: links.map((d) => ({ ...d })), }); }; diff --git a/app/assets/javascripts/pipelines/components/dag/interactions.js b/app/assets/javascripts/pipelines/components/dag/interactions.js index e9f3e9f0e2c..69f36feeee4 100644 --- a/app/assets/javascripts/pipelines/components/dag/interactions.js +++ b/app/assets/javascripts/pipelines/components/dag/interactions.js @@ -13,22 +13,22 @@ export const getLiveLinksAsDict = () => { return Object.fromEntries( getLiveLinks() .data() - .map(d => [d.uid, d]), + .map((d) => [d.uid, d]), ); }; export const currentIsLive = (idx, collection) => getCurrent(idx, collection).classed(IS_HIGHLIGHTED); -const backgroundLinks = selection => selection.style('stroke-opacity', highlightOut); -const backgroundNodes = selection => selection.attr('stroke', '#f2f2f2'); -const foregroundLinks = selection => selection.style('stroke-opacity', highlightIn); -const foregroundNodes = selection => selection.attr('stroke', d => d.color); +const backgroundLinks = (selection) => selection.style('stroke-opacity', highlightOut); +const backgroundNodes = (selection) => selection.attr('stroke', '#f2f2f2'); +const foregroundLinks = (selection) => selection.style('stroke-opacity', highlightIn); +const foregroundNodes = (selection) => selection.attr('stroke', (d) => d.color); const renewLinks = (selection, baseOpacity) => selection.style('stroke-opacity', baseOpacity); -const renewNodes = selection => selection.attr('stroke', d => d.color); +const renewNodes = (selection) => selection.attr('stroke', (d) => d.color); -export const getAllLinkAncestors = node => { +export const getAllLinkAncestors = (node) => { if (node.targetLinks) { - return node.targetLinks.flatMap(n => { + return node.targetLinks.flatMap((n) => { return [n, ...getAllLinkAncestors(n.source)]; }); } @@ -36,11 +36,11 @@ export const getAllLinkAncestors = node => { return []; }; -const getAllNodeAncestors = node => { +const getAllNodeAncestors = (node) => { let allNodes = []; if (node.targetLinks) { - allNodes = node.targetLinks.flatMap(n => { + allNodes = node.targetLinks.flatMap((n) => { return getAllNodeAncestors(n.source); }); } @@ -74,7 +74,7 @@ const highlightPath = (parentLinks, parentNodes) => { }); /* highlight correct nodes */ - parentNodes.forEach(id => { + parentNodes.forEach((id) => { foregroundNodes(d3.select(`#${id}`)).classed(IS_HIGHLIGHTED, true); }); }; @@ -86,7 +86,7 @@ const restoreNodes = () => { rehighlights their nodes. */ - getLiveLinks().each(d => { + getLiveLinks().each((d) => { foregroundNodes(d3.select(`#${d.source.uid}`)).classed(IS_HIGHLIGHTED, true); foregroundNodes(d3.select(`#${d.target.uid}`)).classed(IS_HIGHLIGHTED, true); }); @@ -97,7 +97,7 @@ const restorePath = (parentLinks, parentNodes, baseOpacity) => { renewLinks(d3.select(`#${uid}`), baseOpacity).classed(IS_HIGHLIGHTED, false); }); - parentNodes.forEach(id => { + parentNodes.forEach((id) => { d3.select(`#${id}`).classed(IS_HIGHLIGHTED, false); }); @@ -112,7 +112,7 @@ const restorePath = (parentLinks, parentNodes, baseOpacity) => { restoreNodes(); }; -export const restoreLinks = baseOpacity => { +export const restoreLinks = (baseOpacity) => { /* if there exist live links, reset to highlight out / pale otherwise, reset to base diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index 4e9b21a5c55..0ce94d4f02f 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils'; import { dasherize } from '~/lib/utils/text_utility'; import { __ } from '~/locale'; import { deprecatedCreateFlash as createFlash } from '~/flash'; +import { reportToSentry } from './utils'; /** * Renders either a cancel, retry or play icon button and handles the post request @@ -50,6 +51,9 @@ export default { return `${actionIconDash} js-icon-${actionIconDash}`; }, }, + errorCaptured(err, _vm, info) { + reportToSentry('action_component', `error: ${err}, info: ${info}`); + }, methods: { /** * The request should not be handled here. @@ -70,10 +74,12 @@ export default { this.$emit('pipelineActionRequestComplete'); }) - .catch(() => { + .catch((err) => { this.isDisabled = false; this.isLoading = false; + reportToSentry('action_component', err); + createFlash(__('An error occurred while making the request.')); }); }, diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 67b2ed3b596..cd403757fe6 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,12 +1,15 @@ <script> import LinkedGraphWrapper from '../graph_shared/linked_graph_wrapper.vue'; +import LinksLayer from '../graph_shared/links_layer.vue'; import LinkedPipelinesColumn from './linked_pipelines_column.vue'; import StageColumnComponent from './stage_column_component.vue'; import { DOWNSTREAM, MAIN, UPSTREAM } from './constants'; +import { reportToSentry } from './utils'; export default { name: 'PipelineGraph', components: { + LinksLayer, LinkedGraphWrapper, LinkedPipelinesColumn, StageColumnComponent, @@ -31,9 +34,16 @@ export default { DOWNSTREAM, UPSTREAM, }, + CONTAINER_REF: 'PIPELINE_LINKS_CONTAINER_REF', + BASE_CONTAINER_ID: 'pipeline-links-container', data() { return { hoveredJobName: '', + highlightedJobs: [], + measurements: { + width: 0, + height: 0, + }, pipelineExpanded: { jobName: '', expanded: false, @@ -41,6 +51,9 @@ export default { }; }, computed: { + containerId() { + return `${this.$options.BASE_CONTAINER_ID}-${this.pipeline.id}`; + }, downstreamPipelines() { return this.hasDownstreamPipelines ? this.pipeline.downstream : []; }, @@ -53,12 +66,13 @@ export default { hasUpstreamPipelines() { return Boolean(this.pipeline?.upstream?.length > 0); }, - // The two show checks prevent upstream / downstream from showing redundant linked columns + // The show downstream check prevents showing redundant linked columns showDownstreamPipelines() { return ( this.hasDownstreamPipelines && this.type !== this.$options.pipelineTypeConstants.UPSTREAM ); }, + // The show upstream check prevents showing redundant linked columns showUpstreamPipelines() { return ( this.hasUpstreamPipelines && this.type !== this.$options.pipelineTypeConstants.DOWNSTREAM @@ -68,7 +82,22 @@ export default { return this.hasUpstreamPipelines ? this.pipeline.upstream : []; }, }, + errorCaptured(err, _vm, info) { + reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); + }, + mounted() { + this.measurements = this.getMeasurements(); + }, methods: { + getMeasurements() { + return { + width: this.$refs[this.containerId].scrollWidth, + height: this.$refs[this.containerId].scrollHeight, + }; + }, + onError(errorType) { + this.$emit('error', errorType); + }, setJob(jobName) { this.hoveredJobName = jobName; }, @@ -78,14 +107,17 @@ export default { jobName: expanded ? jobName : '', }; }, + updateHighlightedJobs(jobs) { + this.highlightedJobs = jobs; + }, }, }; </script> <template> <div class="js-pipeline-graph"> <div - class="gl-pipeline-min-h gl-display-flex gl-position-relative gl-overflow-auto gl-bg-gray-10 gl-white-space-nowrap" - :class="{ 'gl-py-5': !isLinkedPipeline }" + class="gl-display-flex gl-position-relative gl-overflow-auto gl-bg-gray-10 gl-white-space-nowrap" + :class="{ 'gl-pipeline-min-h gl-py-5': !isLinkedPipeline }" > <linked-graph-wrapper> <template #upstream> @@ -94,20 +126,36 @@ export default { :linked-pipelines="upstreamPipelines" :column-title="__('Upstream')" :type="$options.pipelineTypeConstants.UPSTREAM" - @error="emit('error', errorType)" + @error="onError" /> </template> <template #main> - <stage-column-component - v-for="stage in graph" - :key="stage.name" - :title="stage.name" - :groups="stage.groups" - :action="stage.status.action" - :job-hovered="hoveredJobName" - :pipeline-expanded="pipelineExpanded" - @refreshPipelineGraph="$emit('refreshPipelineGraph')" - /> + <div :id="containerId" :ref="containerId"> + <links-layer + :pipeline-data="graph" + :pipeline-id="pipeline.id" + :container-id="containerId" + :container-measurements="measurements" + :highlighted-job="hoveredJobName" + default-link-color="gl-stroke-transparent" + @error="onError" + @highlightedJobsChange="updateHighlightedJobs" + > + <stage-column-component + v-for="stage in graph" + :key="stage.name" + :title="stage.name" + :groups="stage.groups" + :action="stage.status.action" + :highlighted-jobs="highlightedJobs" + :job-hovered="hoveredJobName" + :pipeline-expanded="pipelineExpanded" + :pipeline-id="pipeline.id" + @refreshPipelineGraph="$emit('refreshPipelineGraph')" + @jobHover="setJob" + /> + </links-layer> + </div> </template> <template #downstream> <linked-pipelines-column @@ -117,7 +165,7 @@ export default { :type="$options.pipelineTypeConstants.DOWNSTREAM" @downstreamHovered="setJob" @pipelineExpandToggle="togglePipelineExpanded" - @error="emit('error', errorType)" + @error="onError" /> </template> </linked-graph-wrapper> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue index 9ca4dc1e27a..2164dbf4d55 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue @@ -5,6 +5,7 @@ import StageColumnComponentLegacy from './stage_column_component_legacy.vue'; import LinkedPipelinesColumnLegacy from './linked_pipelines_column_legacy.vue'; import GraphBundleMixin from '../../mixins/graph_pipeline_bundle_mixin'; import { UPSTREAM, DOWNSTREAM, MAIN } from './constants'; +import { reportToSentry } from './utils'; export default { name: 'PipelineGraphLegacy', @@ -78,11 +79,11 @@ export default { return ( this.pipeline.triggered_by && Array.isArray(this.pipeline.triggered_by) && - this.pipeline.triggered_by.find(el => el.isExpanded) + this.pipeline.triggered_by.find((el) => el.isExpanded) ); }, expandedDownstream() { - return this.pipeline.triggered && this.pipeline.triggered.find(el => el.isExpanded); + return this.pipeline.triggered && this.pipeline.triggered.find((el) => el.isExpanded); }, pipelineTypeUpstream() { return this.type !== this.$options.downstream && this.expandedUpstream; @@ -94,6 +95,9 @@ export default { return this.pipeline.project.id; }, }, + errorCaptured(err, _vm, info) { + reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); + }, methods: { capitalizeStageName(name) { const escapedName = escape(name); diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue index d98e3aad054..f596333237d 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue @@ -1,10 +1,10 @@ <script> import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; +import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql'; import { __ } from '~/locale'; import { DEFAULT, LOAD_FAILURE } from '../../constants'; -import getPipelineDetails from '../../graphql/queries/get_pipeline_details.query.graphql'; import PipelineGraph from './graph_component.vue'; -import { unwrapPipelineData, toggleQueryPollingByVisibility } from './utils'; +import { unwrapPipelineData, toggleQueryPollingByVisibility, reportToSentry } from './utils'; export default { name: 'PipelineGraphWrapper', @@ -76,6 +76,9 @@ export default { mounted() { toggleQueryPollingByVisibility(this.$apollo.queries.pipeline); }, + errorCaptured(err, _vm, info) { + reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); + }, methods: { hideAlert() { this.showAlert = false; @@ -86,6 +89,7 @@ export default { reportFailure(type) { this.showAlert = true; this.failureType = type; + reportToSentry(this.$options.name, this.failureType); }, }, }; diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue index 203d6a12edd..08d6162aeb8 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue @@ -2,6 +2,7 @@ import { GlTooltipDirective } from '@gitlab/ui'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import JobItem from './job_item.vue'; +import { reportToSentry } from './utils'; /** * Renders the dropdown for the pipeline graph. @@ -22,13 +23,24 @@ export default { type: Object, required: true, }, + pipelineId: { + type: Number, + required: false, + default: -1, + }, }, computed: { + computedJobId() { + return this.pipelineId > -1 ? `${this.group.name}-${this.pipelineId}` : ''; + }, tooltipText() { const { name, status } = this.group; return `${name} - ${status.label}`; }, }, + errorCaptured(err, _vm, info) { + reportToSentry('job_group_dropdown', `error: ${err}, info: ${info}`); + }, methods: { pipelineActionRequestComplete() { this.$emit('pipelineActionRequestComplete'); @@ -37,7 +49,7 @@ export default { }; </script> <template> - <div class="ci-job-dropdown-container dropdown dropright"> + <div :id="computedJobId" class="ci-job-dropdown-container dropdown dropright"> <button v-gl-tooltip.hover="{ boundary: 'viewport' }" :title="tooltipText" diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue index 93ebe02d4e8..8262d728a24 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_item.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue @@ -6,6 +6,7 @@ import { sprintf } from '~/locale'; import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin'; import { accessValue } from './accessors'; import { REST } from './constants'; +import { reportToSentry } from './utils'; /** * Renders the badge for the pipeline graph and the job's dropdown. @@ -73,6 +74,11 @@ export default { required: false, default: () => ({}), }, + pipelineId: { + type: Number, + required: false, + default: -1, + }, }, computed: { boundary() { @@ -84,6 +90,9 @@ export default { hasDetails() { return accessValue(this.dataMethod, 'hasDetails', this.status); }, + computedJobId() { + return this.pipelineId > -1 ? `${this.job.name}-${this.pipelineId}` : ''; + }, status() { return this.job && this.job.status ? this.job.status : {}; }, @@ -130,6 +139,9 @@ export default { : this.cssClassJobName; }, }, + errorCaptured(err, _vm, info) { + reportToSentry('job_item', `error: ${err}, info: ${info}`); + }, methods: { hideTooltips() { this.$root.$emit('bv::hide::tooltip'); @@ -142,6 +154,7 @@ export default { </script> <template> <div + :id="computedJobId" class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between" data-qa-selector="job_item_container" > @@ -151,8 +164,7 @@ export default { :href="detailsPath" :title="tooltipText" :class="jobClasses" - class="js-pipeline-graph-job-link qa-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none - gl-focus-text-decoration-none gl-hover-text-decoration-none" + class="js-pipeline-graph-job-link qa-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none" data-testid="job-with-link" @click.stop="hideTooltips" @mouseout="hideTooltips" diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue index 1a179de64cd..d18e604f087 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue @@ -1,9 +1,10 @@ <script> -import { GlTooltipDirective, GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui'; +import { GlTooltipDirective, GlButton, GlLink, GlLoadingIcon, GlBadge } from '@gitlab/ui'; import CiStatus from '~/vue_shared/components/ci_icon.vue'; import { __, sprintf } from '~/locale'; import { accessValue } from './accessors'; import { DOWNSTREAM, REST, UPSTREAM } from './constants'; +import { reportToSentry } from './utils'; export default { directives: { @@ -14,6 +15,7 @@ export default { GlButton, GlLink, GlLoadingIcon, + GlBadge, }, inject: { dataMethod: { @@ -114,6 +116,9 @@ export default { return this.isUpstream ? 'gl-left-0 gl-border-r-1!' : 'gl-right-0 gl-border-l-1!'; }, }, + errorCaptured(err, _vm, info) { + reportToSentry('linked_pipeline', `error: ${err}, info: ${info}`); + }, methods: { onClickLinkedPipeline() { this.hideTooltips(); @@ -168,7 +173,9 @@ export default { </div> </div> <div class="gl-pt-2"> - <span class="badge badge-primary" data-testid="downstream-pipeline-label">{{ label }}</span> + <gl-badge size="sm" variant="info" data-testid="downstream-pipeline-label"> + {{ label }} + </gl-badge> </div> <gl-button :id="buttonId" diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue index 7d333087874..40e6a01b88c 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue @@ -1,9 +1,9 @@ <script> -import getPipelineDetails from '../../graphql/queries/get_pipeline_details.query.graphql'; +import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql'; import LinkedPipeline from './linked_pipeline.vue'; import { LOAD_FAILURE } from '../../constants'; import { UPSTREAM } from './constants'; -import { unwrapPipelineData, toggleQueryPollingByVisibility } from './utils'; +import { unwrapPipelineData, toggleQueryPollingByVisibility, reportToSentry } from './utils'; export default { components: { @@ -42,8 +42,8 @@ export default { computed: { columnClass() { const positionValues = { - right: 'gl-ml-11', - left: 'gl-mr-7', + right: 'gl-ml-6', + left: 'gl-mr-6', }; return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`; }, @@ -80,8 +80,13 @@ export default { result() { this.loadingPipelineId = null; }, - error() { + error(err, _vm, _key, type) { this.$emit('error', LOAD_FAILURE); + + reportToSentry( + 'linked_pipelines_column', + `error type: ${LOAD_FAILURE}, error: ${err}, apollo error type: ${type}`, + ); }, }); diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue index 7d371b33220..2f1390e07d1 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue @@ -1,6 +1,7 @@ <script> import LinkedPipeline from './linked_pipeline.vue'; import { UPSTREAM } from './constants'; +import { reportToSentry } from './utils'; export default { components: { @@ -42,6 +43,9 @@ export default { return this.type === UPSTREAM; }, }, + errorCaptured(err, _vm, info) { + reportToSentry('linked_pipelines_column_legacy', `error: ${err}, info: ${info}`); + }, methods: { onPipelineClick(downstreamNode, pipeline, index) { this.$emit('linkedPipelineClick', pipeline, index, downstreamNode); diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index b9bddc94ce4..65f8c231885 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -6,6 +6,7 @@ import JobGroupDropdown from './job_group_dropdown.vue'; import ActionComponent from './action_component.vue'; import { GRAPHQL } from './constants'; import { accessValue } from './accessors'; +import { reportToSentry } from './utils'; export default { components: { @@ -15,19 +16,28 @@ export default { MainGraphWrapper, }, props: { - title: { - type: String, - required: true, - }, groups: { type: Array, required: true, }, + pipelineId: { + type: Number, + required: true, + }, + title: { + type: String, + required: true, + }, action: { type: Object, required: false, default: () => ({}), }, + highlightedJobs: { + type: Array, + required: false, + default: () => [], + }, jobHovered: { type: String, required: false, @@ -54,6 +64,9 @@ export default { return !isEmpty(this.action); }, }, + errorCaptured(err, _vm, info) { + reportToSentry('stage_column_component', `error: ${err}, info: ${info}`); + }, methods: { getGroupId(group) { return accessValue(GRAPHQL, 'groupId', group); @@ -61,11 +74,18 @@ export default { groupId(group) { return `ci-badge-${escape(group.name)}`; }, + isFadedOut(jobName) { + return ( + this.jobHovered && + this.highlightedJobs.length > 1 && + !this.highlightedJobs.includes(jobName) + ); + }, }, }; </script> <template> - <main-graph-wrapper> + <main-graph-wrapper class="gl-px-6"> <template #stages> <div data-testid="stage-column-title" @@ -90,16 +110,25 @@ export default { :key="getGroupId(group)" data-testid="stage-column-group" class="gl-relative gl-mb-3 gl-white-space-normal gl-pipeline-job-width" + @mouseenter="$emit('jobHover', group.name)" + @mouseleave="$emit('jobHover', '')" > <job-item v-if="group.size === 1" :job="group.jobs[0]" :job-hovered="jobHovered" :pipeline-expanded="pipelineExpanded" + :pipeline-id="pipelineId" css-class-job-name="gl-build-content" + :class="{ 'gl-opacity-3': isFadedOut(group.name) }" @pipelineActionRequestComplete="$emit('refreshPipelineGraph')" /> - <job-group-dropdown v-else :group="group" /> + <job-group-dropdown + v-else + :group="group" + :pipeline-id="pipelineId" + :class="{ 'gl-opacity-3': isFadedOut(group.name) }" + /> </div> </template> </main-graph-wrapper> diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue index 258b6bf6b6d..059e8f9f8db 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue @@ -4,6 +4,7 @@ import stageColumnMixin from '../../mixins/stage_column_mixin'; import JobItem from './job_item.vue'; import JobGroupDropdown from './job_group_dropdown.vue'; import ActionComponent from './action_component.vue'; +import { reportToSentry } from './utils'; export default { components: { @@ -52,6 +53,9 @@ export default { return !isEmpty(this.action); }, }, + errorCaptured(err, _vm, info) { + reportToSentry('stage_column_component_legacy', `error: ${err}, info: ${info}`); + }, methods: { groupId(group) { return `ci-badge-${escape(group.name)}`; diff --git a/app/assets/javascripts/pipelines/components/graph/utils.js b/app/assets/javascripts/pipelines/components/graph/utils.js index 32588feb426..1a935599bfa 100644 --- a/app/assets/javascripts/pipelines/components/graph/utils.js +++ b/app/assets/javascripts/pipelines/components/graph/utils.js @@ -1,5 +1,6 @@ import Visibility from 'visibilityjs'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import * as Sentry from '~/sentry/wrapper'; import { unwrapStagesWithNeeds } from '../unwrapping_utils'; const addMulti = (mainPipelineProjectPath, linkedPipeline) => { @@ -9,7 +10,7 @@ const addMulti = (mainPipelineProjectPath, linkedPipeline) => { }; }; -const transformId = linkedPipeline => { +const transformId = (linkedPipeline) => { return { ...linkedPipeline, id: getIdFromGraphQLId(linkedPipeline.id) }; }; @@ -42,7 +43,7 @@ const unwrapPipelineData = (mainPipelineProjectPath, data) => { }; const toggleQueryPollingByVisibility = (queryRef, interval = 10000) => { - const stopStartQuery = query => { + const stopStartQuery = (query) => { if (!Visibility.hidden()) { query.startPolling(interval); } else { @@ -55,3 +56,10 @@ const toggleQueryPollingByVisibility = (queryRef, interval = 10000) => { }; export { unwrapPipelineData, toggleQueryPollingByVisibility }; + +export const reportToSentry = (component, failureType) => { + Sentry.withScope((scope) => { + scope.setTag('component', component); + Sentry.captureException(failureType); + }); +}; diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/drawing_utils.js b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js index 35230e1511b..65c215be794 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_graph/drawing_utils.js +++ b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js @@ -1,5 +1,7 @@ import * as d3 from 'd3'; -import { createUniqueLinkId } from '../../utils'; + +export const createUniqueLinkId = (stageName, jobName) => `${stageName}-${jobName}`; + /** * This function expects its first argument data structure * to be the same shaped as the one generated by `parseData`, @@ -7,21 +9,23 @@ import { createUniqueLinkId } from '../../utils'; * we find the nodes in the graph, calculate their coordinates and * trace the lines that represent the needs of each job. * @param {Object} nodeDict - Resulting object of `parseData` with nodes and links - * @param {Object} jobs - An object where each key is the job name that contains the job data - * @param {ref} svg - Reference to the svg we draw in + * @param {String} containerID - Id for the svg the links will be draw in * @returns {Array} Links that contain all the information about them */ -export const generateLinksData = ({ links }, containerID) => { +export const generateLinksData = ({ links }, containerID, modifier = '') => { const containerEl = document.getElementById(containerID); - return links.map(link => { + return links.map((link) => { const path = d3.path(); const sourceId = link.source; const targetId = link.target; - const sourceNodeEl = document.getElementById(sourceId); - const targetNodeEl = document.getElementById(targetId); + const modifiedSourceId = `${sourceId}${modifier}`; + const modifiedTargetId = `${targetId}${modifier}`; + + const sourceNodeEl = document.getElementById(modifiedSourceId); + const targetNodeEl = document.getElementById(modifiedTargetId); const sourceNodeCoordinates = sourceNodeEl.getBoundingClientRect(); const targetNodeCoordinates = targetNodeEl.getBoundingClientRect(); @@ -35,17 +39,11 @@ export const generateLinksData = ({ links }, containerID) => { // from the total to make sure it's aligned properly. We then make the line // positioned in the center of the job node by adding half the height // of the job pill. - const paddingLeft = Number( - window - .getComputedStyle(containerEl, null) - .getPropertyValue('padding-left') - .replace('px', ''), + const paddingLeft = parseFloat( + window.getComputedStyle(containerEl, null).getPropertyValue('padding-left'), ); - const paddingTop = Number( - window - .getComputedStyle(containerEl, null) - .getPropertyValue('padding-top') - .replace('px', ''), + const paddingTop = parseFloat( + window.getComputedStyle(containerEl, null).getPropertyValue('padding-top'), ); const sourceNodeX = sourceNodeCoordinates.right - containerCoordinates.x - paddingLeft; @@ -66,7 +64,10 @@ export const generateLinksData = ({ links }, containerID) => { // Make cross-stages lines a straight line all the way // until we can safely draw the bezier to look nice. - const straightLineDestinationX = targetNodeX - 100; + // The adjustment number here is a magic number to make things + // look nice and should change if the padding changes. This goes well + // with gl-px-6. gl-px-8 is more like 100. + const straightLineDestinationX = targetNodeX - 60; const controlPointX = straightLineDestinationX + (targetNodeX - straightLineDestinationX) / 2; if (straightLineDestinationX > 0) { diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue new file mode 100644 index 00000000000..89444076ae0 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue @@ -0,0 +1,140 @@ +<script> +import { isEmpty } from 'lodash'; +import { DRAW_FAILURE } from '../../constants'; +import { createJobsHash, generateJobNeedsDict } from '../../utils'; +import { parseData } from '../parsing_utils'; +import { generateLinksData } from './drawing_utils'; + +export default { + name: 'LinksInner', + STROKE_WIDTH: 2, + props: { + containerId: { + type: String, + required: true, + }, + containerMeasurements: { + type: Object, + required: true, + }, + pipelineId: { + type: Number, + required: true, + }, + pipelineData: { + type: Array, + required: true, + }, + defaultLinkColor: { + type: String, + required: false, + default: 'gl-stroke-gray-200', + }, + highlightedJob: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + links: [], + needsObject: null, + }; + }, + computed: { + hasHighlightedJob() { + return Boolean(this.highlightedJob); + }, + isPipelineDataEmpty() { + return isEmpty(this.pipelineData); + }, + highlightedJobs() { + // If you are hovering on a job, then the jobs we want to highlight are: + // The job you are currently hovering + all of its needs. + return this.hasHighlightedJob + ? [this.highlightedJob, ...this.needsObject[this.highlightedJob]] + : []; + }, + highlightedLinks() { + // If you are hovering on a job, then the links we want to highlight are: + // All the links whose `source` and `target` are highlighted jobs. + if (this.hasHighlightedJob) { + const filteredLinks = this.links.filter((link) => { + return ( + this.highlightedJobs.includes(link.source) && this.highlightedJobs.includes(link.target) + ); + }); + + return filteredLinks.map((link) => link.ref); + } + + return []; + }, + viewBox() { + return [0, 0, this.containerMeasurements.width, this.containerMeasurements.height]; + }, + }, + watch: { + highlightedJob() { + // On first hover, generate the needs reference + if (!this.needsObject) { + const jobs = createJobsHash(this.pipelineData); + this.needsObject = generateJobNeedsDict(jobs) ?? {}; + } + }, + highlightedJobs(jobs) { + this.$emit('highlightedJobsChange', jobs); + }, + }, + mounted() { + if (!isEmpty(this.pipelineData)) { + this.prepareLinkData(); + } + }, + methods: { + isLinkHighlighted(linkRef) { + return this.highlightedLinks.includes(linkRef); + }, + prepareLinkData() { + try { + const arrayOfJobs = this.pipelineData.flatMap(({ groups }) => groups); + const parsedData = parseData(arrayOfJobs); + this.links = generateLinksData(parsedData, this.containerId, `-${this.pipelineId}`); + } catch { + this.$emit('error', DRAW_FAILURE); + } + }, + getLinkClasses(link) { + return [ + this.isLinkHighlighted(link.ref) ? 'gl-stroke-blue-400' : this.defaultLinkColor, + { 'gl-opacity-3': this.hasHighlightedJob && !this.isLinkHighlighted(link.ref) }, + ]; + }, + }, +}; +</script> +<template> + <div class="gl-display-flex gl-relative"> + <svg + id="link-svg" + class="gl-absolute" + :viewBox="viewBox" + :width="`${containerMeasurements.width}px`" + :height="`${containerMeasurements.height}px`" + > + <template> + <path + v-for="link in links" + :key="link.path" + :ref="link.ref" + :d="link.path" + class="gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease" + :class="getLinkClasses(link)" + :stroke-width="$options.STROKE_WIDTH" + /> + </template> + </svg> + <slot></slot> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue new file mode 100644 index 00000000000..0993892a574 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue @@ -0,0 +1,86 @@ +<script> +import { GlAlert } from '@gitlab/ui'; +import { __ } from '~/locale'; +import LinksInner from './links_inner.vue'; + +export default { + name: 'LinksLayer', + components: { + GlAlert, + LinksInner, + }, + MAX_GROUPS: 200, + props: { + containerMeasurements: { + type: Object, + required: true, + }, + pipelineData: { + type: Array, + required: true, + }, + }, + data() { + return { + alertDismissed: false, + showLinksOverride: false, + }; + }, + i18n: { + showLinksAnyways: __('Show links anyways'), + tooManyJobs: __( + 'This graph has a large number of jobs and showing the links between them may have performance implications.', + ), + }, + computed: { + containerZero() { + return !this.containerMeasurements.width || !this.containerMeasurements.height; + }, + numGroups() { + return this.pipelineData.reduce((acc, { groups }) => { + return acc + Number(groups.length); + }, 0); + }, + showAlert() { + return !this.showLinkedLayers && !this.alertDismissed; + }, + showLinkedLayers() { + return ( + !this.containerZero && (this.showLinksOverride || this.numGroups < this.$options.MAX_GROUPS) + ); + }, + }, + methods: { + dismissAlert() { + this.alertDismissed = true; + }, + overrideShowLinks() { + this.dismissAlert(); + this.showLinksOverride = true; + }, + }, +}; +</script> +<template> + <links-inner + v-if="showLinkedLayers" + :container-measurements="containerMeasurements" + :pipeline-data="pipelineData" + v-bind="$attrs" + v-on="$listeners" + > + <slot></slot> + </links-inner> + <div v-else> + <gl-alert + v-if="showAlert" + class="gl-w-max-content gl-ml-4" + :primary-button-text="$options.i18n.showLinksAnyways" + @primaryAction="overrideShowLinks" + @dismiss="dismissAlert" + > + {{ $options.i18n.tooManyJobs }} + </gl-alert> + <slot></slot> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue b/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue index 1c9e3236d56..bcd7705669e 100644 --- a/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue +++ b/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue @@ -16,14 +16,11 @@ export default { </script> <template> <div> - <div - class="gl-display-flex gl-align-items-center gl-w-full gl-px-8 gl-mb-5" - :class="stageClasses" - > + <div class="gl-display-flex gl-align-items-center gl-w-full gl-mb-5" :class="stageClasses"> <slot name="stages"> </slot> </div> <div - class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full gl-px-8" + class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full" :class="jobClasses" > <slot name="jobs"> </slot> diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index af7c0d0ec3f..a20bd70e90a 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -54,7 +54,7 @@ export default { iid: this.pipelineIid, }; }, - update: data => data.project.pipeline, + update: (data) => data.project.pipeline, error() { this.reportFailure(LOAD_FAILURE); }, diff --git a/app/assets/javascripts/pipelines/components/parsing_utils.js b/app/assets/javascripts/pipelines/components/parsing_utils.js index 1ed415688f2..9c97fa832d0 100644 --- a/app/assets/javascripts/pipelines/components/parsing_utils.js +++ b/app/assets/javascripts/pipelines/components/parsing_utils.js @@ -33,15 +33,15 @@ import { uniqWith, isEqual } from 'lodash'; 10 -> value (constant) */ -export const createNodeDict = nodes => { +export const createNodeDict = (nodes) => { return nodes.reduce((acc, node) => { const newNode = { ...node, - needs: node.jobs.map(job => job.needs || []).flat(), + needs: node.jobs.map((job) => job.needs || []).flat(), }; if (node.size > 1) { - node.jobs.forEach(job => { + node.jobs.forEach((job) => { acc[job.name] = newNode; }); } @@ -54,13 +54,13 @@ export const createNodeDict = nodes => { export const makeLinksFromNodes = (nodes, nodeDict) => { const constantLinkValue = 10; // all links are the same weight return nodes - .map(group => { - return group.jobs.map(job => { + .map((group) => { + return group.jobs.map((job) => { if (!job.needs) { return []; } - return job.needs.map(needed => { + return job.needs.map((needed) => { return { source: nodeDict[needed]?.name, target: group.name, @@ -74,7 +74,7 @@ export const makeLinksFromNodes = (nodes, nodeDict) => { export const getAllAncestors = (nodes, nodeDict) => { const needs = nodes - .map(node => { + .map((node) => { return nodeDict[node].needs || ''; }) .flat() @@ -102,13 +102,13 @@ export const filterByAncestors = (links, nodeDict) => */ const targetNode = target; const targetNodeNeeds = nodeDict[targetNode].needs; - const targetNodeNeedsMinusSource = targetNodeNeeds.filter(need => need !== source); + const targetNodeNeedsMinusSource = targetNodeNeeds.filter((need) => need !== source); const allAncestors = getAllAncestors(targetNodeNeedsMinusSource, nodeDict); return !allAncestors.includes(source); }); -export const parseData = nodes => { +export const parseData = (nodes) => { const nodeDict = createNodeDict(nodes); const allLinks = makeLinksFromNodes(nodes, nodeDict); const filteredLinks = filterByAncestors(allLinks, nodeDict); @@ -121,7 +121,7 @@ export const parseData = nodes => { The number of nodes in the most populous generation drives the height of the graph. */ -export const getMaxNodes = nodes => { +export const getMaxNodes = (nodes) => { const counts = nodes.reduce((acc, { layer }) => { if (!acc[layer]) { acc[layer] = 0; @@ -141,6 +141,6 @@ export const getMaxNodes = nodes => { to find nodes that have no relations. */ -export const removeOrphanNodes = sankeyfiedNodes => { - return sankeyfiedNodes.filter(node => node.sourceLinks.length || node.targetLinks.length); +export const removeOrphanNodes = (sankeyfiedNodes) => { + return sankeyfiedNodes.filter((node) => node.sourceLinks.length || node.targetLinks.length); }; diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue index 73e5f2542fb..8636808b69e 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue @@ -1,12 +1,10 @@ <script> -import { isEmpty } from 'lodash'; import { GlAlert } from '@gitlab/ui'; import { __ } from '~/locale'; +import { generateLinksData } from '../graph_shared/drawing_utils'; import JobPill from './job_pill.vue'; import StagePill from './stage_pill.vue'; -import { generateLinksData } from './drawing_utils'; import { parseData } from '../parsing_utils'; -import { unwrapArrayOfJobs } from '../unwrapping_utils'; import { DRAW_FAILURE, DEFAULT, INVALID_CI_CONFIG, EMPTY_PIPELINE_DATA } from '../../constants'; import { createJobsHash, generateJobNeedsDict } from '../../utils'; import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants'; @@ -23,8 +21,6 @@ export default { errorTexts: { [DRAW_FAILURE]: __('Could not draw the lines for job relationships'), [DEFAULT]: __('An unknown error occurred.'), - }, - warningTexts: { [EMPTY_PIPELINE_DATA]: __( 'The visualization will appear in this tab when the CI/CD configuration file is populated with valid syntax.', ), @@ -47,21 +43,24 @@ export default { }; }, computed: { + hideGraph() { + // We won't even try to render the graph with these condition + // because it would cause additional errors down the line for the user + // which is confusing. + return this.isPipelineDataEmpty || this.isInvalidCiConfig; + }, + pipelineStages() { + return this.pipelineData?.stages || []; + }, isPipelineDataEmpty() { - return !this.isInvalidCiConfig && isEmpty(this.pipelineData?.stages); + return !this.isInvalidCiConfig && this.pipelineStages.length === 0; }, isInvalidCiConfig() { return this.pipelineData?.status === CI_CONFIG_STATUS_INVALID; }, - showAlert() { - return this.hasError || this.hasWarning; - }, hasError() { return this.failureType; }, - hasWarning() { - return this.warning; - }, hasHighlightedJob() { return Boolean(this.highlightedJob); }, @@ -73,26 +72,32 @@ export default { return this.warning; }, failure() { - const text = this.$options.errorTexts[this.failureType] || this.$options.errorTexts[DEFAULT]; - - return { text, variant: 'danger', dismissible: true }; - }, - warning() { - if (this.isPipelineDataEmpty) { - return { - text: this.$options.warningTexts[EMPTY_PIPELINE_DATA], - variant: 'tip', - dismissible: false, - }; - } else if (this.isInvalidCiConfig) { - return { - text: this.$options.warningTexts[INVALID_CI_CONFIG], - variant: 'danger', - dismissible: false, - }; + switch (this.failureType) { + case DRAW_FAILURE: + return { + text: this.$options.errorTexts[DRAW_FAILURE], + variant: 'danger', + dismissible: true, + }; + case EMPTY_PIPELINE_DATA: + return { + text: this.$options.errorTexts[EMPTY_PIPELINE_DATA], + variant: 'tip', + dismissible: false, + }; + case INVALID_CI_CONFIG: + return { + text: this.$options.errorTexts[INVALID_CI_CONFIG], + variant: 'danger', + dismissible: false, + }; + default: + return { + text: this.$options.errorTexts[DEFAULT], + variant: 'danger', + dismissible: true, + }; } - - return null; }, viewBox() { return [0, 0, this.width, this.height]; @@ -100,40 +105,45 @@ export default { highlightedJobs() { // If you are hovering on a job, then the jobs we want to highlight are: // The job you are currently hovering + all of its needs. - return this.hasHighlightedJob - ? [this.highlightedJob, ...this.needsObject[this.highlightedJob]] - : []; + return [this.highlightedJob, ...this.needsObject[this.highlightedJob]]; }, highlightedLinks() { // If you are hovering on a job, then the links we want to highlight are: // All the links whose `source` and `target` are highlighted jobs. if (this.hasHighlightedJob) { - const filteredLinks = this.links.filter(link => { + const filteredLinks = this.links.filter((link) => { return ( this.highlightedJobs.includes(link.source) && this.highlightedJobs.includes(link.target) ); }); - return filteredLinks.map(link => link.ref); + return filteredLinks.map((link) => link.ref); } return []; }, }, - mounted() { - if (!this.isPipelineDataEmpty && !this.isInvalidCiConfig) { - // This guarantee that all sub-elements are rendered - // https://v3.vuejs.org/api/options-lifecycle-hooks.html#mounted - this.$nextTick(() => { - this.getGraphDimensions(); - this.prepareLinkData(); - }); - } + watch: { + pipelineData: { + immediate: true, + handler() { + if (this.isPipelineDataEmpty) { + this.reportFailure(EMPTY_PIPELINE_DATA); + } else if (this.isInvalidCiConfig) { + this.reportFailure(INVALID_CI_CONFIG); + } else { + this.$nextTick(() => { + this.computeGraphDimensions(); + this.prepareLinkData(); + }); + } + }, + }, }, methods: { prepareLinkData() { try { - const arrayOfJobs = unwrapArrayOfJobs(this.pipelineData); + const arrayOfJobs = this.pipelineStages.flatMap(({ groups }) => groups); const parsedData = parseData(arrayOfJobs); this.links = generateLinksData(parsedData, this.$options.CONTAINER_ID); } catch { @@ -141,7 +151,7 @@ export default { } }, getStageBackgroundClasses(index) { - const { length } = this.pipelineData.stages; + const { length } = this.pipelineStages; // It's possible for a graph to have only one stage, in which // case we concatenate both the left and right rounding classes if (length === 1) { @@ -162,7 +172,7 @@ export default { // The first time we hover, we create the object where // we store all the data to properly highlight the needs. if (!this.needsObject) { - const jobs = createJobsHash(this.pipelineData); + const jobs = createJobsHash(this.pipelineStages); this.needsObject = generateJobNeedsDict(jobs) ?? {}; } @@ -171,7 +181,7 @@ export default { removeHighlightNeeds() { this.highlightedJob = null; }, - getGraphDimensions() { + computeGraphDimensions() { this.width = `${this.$refs[this.$options.CONTAINER_REF].scrollWidth}`; this.height = `${this.$refs[this.$options.CONTAINER_REF].scrollHeight}`; }, @@ -199,7 +209,7 @@ export default { <template> <div> <gl-alert - v-if="showAlert" + v-if="hasError" :variant="alert.variant" :dismissible="alert.dismissible" @dismiss="alert.dismissible ? resetFailure : null" @@ -207,7 +217,7 @@ export default { {{ alert.text }} </gl-alert> <div - v-if="!hasWarning" + v-if="!hideGraph" :id="$options.CONTAINER_ID" :ref="$options.CONTAINER_REF" class="gl-display-flex gl-bg-gray-50 gl-px-4 gl-overflow-auto gl-relative gl-py-7" @@ -227,7 +237,7 @@ export default { </template> </svg> <div - v-for="(stage, index) in pipelineData.stages" + v-for="(stage, index) in pipelineStages" :key="`${stage.name}-${index}`" class="gl-flex-direction-column" > diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue index 78b69073cd3..ee26ea2f007 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue @@ -1,7 +1,25 @@ <script> import { GlButton } from '@gitlab/ui'; +import { isExperimentEnabled } from '~/lib/utils/experimentation'; +import { s__ } from '~/locale'; +import Tracking from '~/tracking'; export default { + i18n: { + control: { + infoMessage: s__(`Pipelines|Continuous Integration can help + catch bugs by running your tests automatically, + while Continuous Deployment can help you deliver + code to your product environment.`), + buttonMessage: s__('Pipelines|Get started with Pipelines'), + }, + experiment: { + infoMessage: s__(`Pipelines|GitLab CI/CD can automatically build, + test, and deploy your code. Let GitLab take care of time + consuming tasks, so you can spend more time creating.`), + buttonMessage: s__('Pipelines|Get started with CI/CD'), + }, + }, name: 'PipelinesEmptyState', components: { GlButton, @@ -20,6 +38,23 @@ export default { required: true, }, }, + mounted() { + this.track('viewed'); + }, + methods: { + track(action) { + if (!gon.tracking_data) { + return; + } + + const { category, value, label, property } = gon.tracking_data; + + Tracking.event(category, action, { value, label, property }); + }, + isExperimentEnabled() { + return isExperimentEnabled('pipelinesEmptyState'); + }, + }, }; </script> <template> @@ -29,18 +64,16 @@ export default { </div> <div class="col-12"> - <div class="gl-text-content"> + <div class="text-content"> <template v-if="canSetCi"> - <h4 class="gl-text-center" data-testid="header-text"> + <h4 data-testid="header-text" class="gl-text-center"> {{ s__('Pipelines|Build with confidence') }} </h4> - <p data-testid="info-text"> {{ - s__(`Pipelines|Continuous Integration can help - catch bugs by running your tests automatically, - while Continuous Deployment can help you deliver - code to your product environment.`) + isExperimentEnabled() + ? $options.i18n.experiment.infoMessage + : $options.i18n.control.infoMessage }} </p> @@ -50,8 +83,13 @@ export default { variant="info" category="primary" data-testid="get-started-pipelines" + @click="track('documentation_clicked')" > - {{ s__('Pipelines|Get started with Pipelines') }} + {{ + isExperimentEnabled() + ? $options.i18n.experiment.buttonMessage + : $options.i18n.control.buttonMessage + }} </gl-button> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue index bde0dd53aac..d1bac078642 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue @@ -1,5 +1,5 @@ <script> -import { GlLink, GlPopover, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; +import { GlLink, GlPopover, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui'; import { SCHEDULE_ORIGIN } from '../../constants'; export default { @@ -7,10 +7,16 @@ export default { GlLink, GlPopover, GlSprintf, + GlBadge, }, directives: { GlTooltip: GlTooltipDirective, }, + inject: { + targetProjectFullPath: { + default: '', + }, + }, props: { pipeline: { type: Object, @@ -25,11 +31,6 @@ export default { required: true, }, }, - inject: { - targetProjectFullPath: { - default: '', - }, - }, computed: { user() { return this.pipeline.user; @@ -50,7 +51,6 @@ export default { <div class="table-section section-10 d-none d-md-block pipeline-tags"> <gl-link :href="pipeline.path" - class="js-pipeline-url-link js-onboarding-pipeline-item" data-testid="pipeline-url-link" data-qa-selector="pipeline_url_link" > @@ -58,46 +58,49 @@ export default { </gl-link> <div class="label-container"> <gl-link v-if="isScheduled" :href="pipelineScheduleUrl" target="__blank"> - <span + <gl-badge v-gl-tooltip :title="__('This pipeline was triggered by a schedule.')" - class="badge badge-info" + variant="info" + size="sm" data-testid="pipeline-url-scheduled" - >{{ __('Scheduled') }}</span + >{{ __('Scheduled') }}</gl-badge > </gl-link> - <span + <gl-badge v-if="pipeline.flags.latest" v-gl-tooltip :title="__('Latest pipeline for the most recent commit on this branch')" - class="js-pipeline-url-latest badge badge-success" + variant="success" + size="sm" data-testid="pipeline-url-latest" - >{{ __('latest') }}</span + >{{ __('latest') }}</gl-badge > - <span + <gl-badge v-if="pipeline.flags.yaml_errors" v-gl-tooltip :title="pipeline.yaml_errors" - class="js-pipeline-url-yaml badge badge-danger" + variant="danger" + size="sm" data-testid="pipeline-url-yaml" - >{{ __('yaml invalid') }}</span + >{{ __('yaml invalid') }}</gl-badge > - <span + <gl-badge v-if="pipeline.flags.failure_reason" v-gl-tooltip :title="pipeline.failure_reason" - class="js-pipeline-url-failure badge badge-danger" + variant="danger" + size="sm" data-testid="pipeline-url-failure" - >{{ __('error') }}</span + >{{ __('error') }}</gl-badge > <gl-link v-if="pipeline.flags.auto_devops" :id="`pipeline-url-autodevops-${pipeline.id}`" tabindex="0" - class="js-pipeline-url-autodevops badge badge-info autodevops-badge" data-testid="pipeline-url-autodevops" role="button" - >{{ __('Auto DevOps') }}</gl-link + ><gl-badge variant="info" size="sm">{{ __('Auto DevOps') }}</gl-badge></gl-link > <gl-popover :target="`pipeline-url-autodevops-${pipeline.id}`" @@ -113,7 +116,7 @@ export default { ) " > - <template #strong="{content}"> + <template #strong="{ content }"> <b>{{ content }}</b> </template> </gl-sprintf> @@ -123,13 +126,14 @@ export default { __('Learn more about Auto DevOps') }}</gl-link> </gl-popover> - <span + <gl-badge v-if="pipeline.flags.stuck" - class="js-pipeline-url-stuck badge badge-warning" + variant="warning" + size="sm" data-testid="pipeline-url-stuck" - >{{ __('stuck') }}</span + >{{ __('stuck') }}</gl-badge > - <span + <gl-badge v-if="pipeline.flags.detached_merge_request_pipeline" v-gl-tooltip :title=" @@ -137,17 +141,19 @@ export default { 'Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for Pipelines for Merged Results.', ) " - class="js-pipeline-url-detached badge badge-info" + variant="info" + size="sm" data-testid="pipeline-url-detached" - >{{ __('detached') }}</span + >{{ __('detached') }}</gl-badge > - <span + <gl-badge v-if="isInFork" v-gl-tooltip :title="__('Pipeline ran in fork of project')" - class="badge badge-info" + variant="info" + size="sm" data-testid="pipeline-url-fork" - >{{ __('fork') }}</span + >{{ __('fork') }}</gl-badge > </div> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue index ff27226b408..ec7c5764be1 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue @@ -246,7 +246,7 @@ export default { filterPipelines(filters) { this.resetRequestData(); - filters.forEach(filter => { + filters.forEach((filter) => { // do not add Any for username query param, so we // can fetch all trigger authors if ( @@ -279,7 +279,7 @@ export default { <div class="pipelines-container"> <div v-if="shouldRenderTabs || shouldRenderButtons" - class="top-area scrolling-tabs-container inner-page-scroll-tabs" + class="top-area scrolling-tabs-container inner-page-scroll-tabs gl-border-none" > <div class="fade-left"><gl-icon name="chevron-lg-left" :size="12" /></div> <div class="fade-right"><gl-icon name="chevron-lg-right" :size="12" /></div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue index 55c71e299be..b13460b4c68 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue @@ -1,14 +1,19 @@ <script> -/* eslint-disable @gitlab/vue-require-i18n-strings */ -import { GlLink, GlTooltipDirective, GlIcon } from '@gitlab/ui'; +import { GlDropdown, GlDropdownItem, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; +import { __ } from '~/locale'; export default { directives: { GlTooltip: GlTooltipDirective, }, components: { - GlIcon, - GlLink, + GlDropdown, + GlDropdownItem, + GlSprintf, + }, + translations: { + artifacts: __('Artifacts'), + downloadArtifact: __('Download %{name} artifact'), }, props: { artifacts: { @@ -19,24 +24,25 @@ export default { }; </script> <template> - <div class="btn-group" role="group"> - <button - v-gl-tooltip - type="button" - class="dropdown-toggle build-artifacts btn btn-default js-pipeline-dropdown-download" - :title="__('Artifacts')" - data-toggle="dropdown" - :aria-label="__('Artifacts')" + <gl-dropdown + v-gl-tooltip + class="build-artifacts js-pipeline-dropdown-download" + :title="$options.translations.artifacts" + :text="$options.translations.artifacts" + :aria-label="$options.translations.artifacts" + icon="download" + text-sr-only + > + <gl-dropdown-item + v-for="(artifact, i) in artifacts" + :key="i" + :href="artifact.path" + rel="nofollow" + download > - <gl-icon name="download" /> - <gl-icon name="chevron-down" /> - </button> - <ul class="dropdown-menu dropdown-menu-right"> - <li v-for="(artifact, i) in artifacts" :key="i"> - <gl-link :href="artifact.path" rel="nofollow" download - >Download {{ artifact.name }} artifact</gl-link - > - </li> - </ul> - </div> + <gl-sprintf :message="$options.translations.downloadArtifact"> + <template #name>{{ artifact.name }}</template> + </gl-sprintf> + </gl-dropdown-item> + </gl-dropdown> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue index 29345f33367..127503f1307 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue @@ -33,7 +33,7 @@ export default { }, computed: { selectedTypes() { - return this.value.map(i => i.type); + return this.value.map((i) => i.type); }, tokens() { return [ diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue index 7224ec455f6..b6c4e617a90 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue @@ -346,7 +346,6 @@ export default { <pipelines-artifacts-component v-if="pipeline.details.artifacts.length" :artifacts="pipeline.details.artifacts" - class="d-md-block" /> <gl-button diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue index 581ea5fbb35..a9154d93194 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue @@ -124,7 +124,7 @@ export default { $( '.js-builds-dropdown-list button, .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item', this.$el, - ).on('click', e => { + ).on('click', (e) => { e.stopPropagation(); }); }, diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue index 60cb697f1af..24456574a6f 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue @@ -34,10 +34,10 @@ export default { fetchBranches(searchterm) { Api.branches(this.config.projectId, searchterm) .then(({ data }) => { - this.branches = data.map(branch => branch.name); + this.branches = data.map((branch) => branch.name); this.loading = false; }) - .catch(err => { + .catch((err) => { createFlash(FETCH_BRANCH_ERROR_MESSAGE); this.loading = false; throw err; diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue index dc43d94f4fd..020a08b8cee 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue @@ -72,7 +72,7 @@ export default { ]; }, findActiveStatus() { - return this.statuses.find(status => status.value === this.value.data); + return this.statuses.find((status) => status.value === this.value.data); }, }, }; diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue index d6ba5fcca85..1241803c612 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue @@ -34,10 +34,10 @@ export default { fetchTags(searchTerm) { Api.tags(this.config.projectId, searchTerm) .then(({ data }) => { - this.tags = data.map(tag => tag.name); + this.tags = data.map((tag) => tag.name); this.loading = false; }) - .catch(err => { + .catch((err) => { createFlash(FETCH_TAG_ERROR_MESSAGE); this.loading = false; throw err; diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue index ae5758233bc..3db5893b565 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue @@ -45,7 +45,7 @@ export default { return this.value.data.toLowerCase(); }, activeUser() { - return this.users.find(user => { + return this.users.find((user) => { return user.username.toLowerCase() === this.currentValue; }); }, @@ -56,11 +56,11 @@ export default { methods: { fetchProjectUsers(searchTerm) { Api.projectUsers(this.config.projectId, searchTerm) - .then(users => { + .then((users) => { this.users = users; this.loading = false; }) - .catch(err => { + .catch((err) => { createFlash(FETCH_AUTHOR_ERROR_MESSAGE); this.loading = false; throw err; @@ -80,7 +80,7 @@ export default { v-on="$listeners" @input="searchAuthors" > - <template #view="{inputValue}"> + <template #view="{ inputValue }"> <gl-avatar v-if="activeUser" :size="16" diff --git a/app/assets/javascripts/pipelines/components/unwrapping_utils.js b/app/assets/javascripts/pipelines/components/unwrapping_utils.js index aa33f622ce6..15073079c0a 100644 --- a/app/assets/javascripts/pipelines/components/unwrapping_utils.js +++ b/app/assets/javascripts/pipelines/components/unwrapping_utils.js @@ -1,22 +1,5 @@ -/** - * This function takes the stages and add the stage name - * at the group level as `category` to have an easier - * implementation while constructions nodes with D3 - * @param {Array} stages - * @returns {Array} - Array of stages with stage name at the group level as `category` - */ -export const unwrapArrayOfJobs = (stages = []) => { - return stages - .map(({ name, groups }) => { - return groups.map(group => { - return { category: name, ...group }; - }); - }) - .flat(2); -}; - -const unwrapGroups = stages => { - return stages.map(stage => { +const unwrapGroups = (stages) => { + return stages.map((stage) => { const { groups: { nodes: groups }, } = stage; @@ -25,21 +8,21 @@ const unwrapGroups = stages => { }; const unwrapNodesWithName = (jobArray, prop, field = 'name') => { - return jobArray.map(job => { - return { ...job, [prop]: job[prop].nodes.map(item => item[field]) }; + return jobArray.map((job) => { + return { ...job, [prop]: job[prop].nodes.map((item) => item[field]) }; }); }; -const unwrapJobWithNeeds = denodedJobArray => { +const unwrapJobWithNeeds = (denodedJobArray) => { return unwrapNodesWithName(denodedJobArray, 'needs'); }; -const unwrapStagesWithNeeds = denodedStages => { +const unwrapStagesWithNeeds = (denodedStages) => { const unwrappedNestedGroups = unwrapGroups(denodedStages); - const nodes = unwrappedNestedGroups.map(node => { + const nodes = unwrappedNestedGroups.map((node) => { const { groups } = node; - const groupsWithJobs = groups.map(group => { + const groupsWithJobs = groups.map((group) => { const jobs = unwrapJobWithNeeds(group.jobs.nodes); return { ...group, jobs }; }); diff --git a/app/assets/javascripts/pipelines/graphql/fragments/linked_pipelines.fragment.graphql b/app/assets/javascripts/pipelines/graphql/fragments/linked_pipelines.fragment.graphql deleted file mode 100644 index 3bf6d8dc9d8..00000000000 --- a/app/assets/javascripts/pipelines/graphql/fragments/linked_pipelines.fragment.graphql +++ /dev/null @@ -1,17 +0,0 @@ -fragment LinkedPipelineData on Pipeline { - id - iid - path - status: detailedStatus { - group - label - icon - } - sourceJob { - name - } - project { - name - fullPath - } -} diff --git a/app/assets/javascripts/pipelines/graphql/queries/pipeline_stages_connection.fragment.graphql b/app/assets/javascripts/pipelines/graphql/fragments/pipeline_stages_connection.fragment.graphql index 1da4fa0a72b..f93908aeb04 100644 --- a/app/assets/javascripts/pipelines/graphql/queries/pipeline_stages_connection.fragment.graphql +++ b/app/assets/javascripts/pipelines/graphql/fragments/pipeline_stages_connection.fragment.graphql @@ -4,9 +4,23 @@ fragment PipelineStagesConnection on CiConfigStageConnection { groups { nodes { name + size jobs { nodes { name + script + beforeScript + afterScript + environment + allowFailure + tags + when + only { + refs + } + except { + refs + } needs { nodes { name diff --git a/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js b/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js index bd1b1664a1e..9f15b6c4ae3 100644 --- a/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js +++ b/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js @@ -6,7 +6,7 @@ export default { getExpandedPipelines(pipeline) { this.mediator.service .getPipeline(this.mediator.getExpandedParameters()) - .then(response => { + .then((response) => { this.mediator.store.toggleLoading(pipeline); this.mediator.store.storePipeline(response.data); this.mediator.poll.enable({ data: this.mediator.getExpandedParameters() }); diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js index e31545bba5c..22cdb6b8f72 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -90,7 +90,7 @@ export default { // fetch new data return this.service .getPipelines(this.requestData) - .then(response => { + .then((response) => { this.isLoading = false; this.successCallback(response); @@ -124,8 +124,8 @@ export default { getPipelines() { return this.service .getPipelines(this.requestData) - .then(response => this.successCallback(response)) - .catch(error => this.errorCallback(error)); + .then((response) => this.successCallback(response)) + .catch((error) => this.errorCallback(error)); }, setCommonData(pipelines) { this.store.storePipelines(pipelines); diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index 27f71d2b878..133608b9801 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -10,6 +10,7 @@ import legacyPipelineHeader from './components/legacy_header_component.vue'; import eventHub from './event_hub'; import TestReports from './components/test_reports/test_reports.vue'; import createTestReportsStore from './stores/test_reports'; +import { reportToSentry } from './components/graph/utils'; Vue.use(Translate); @@ -20,7 +21,7 @@ const SELECTORS = { PIPELINE_TESTS: '#js-pipeline-tests-detail', }; -const createLegacyPipelinesDetailApp = mediator => { +const createLegacyPipelinesDetailApp = (mediator) => { if (!document.querySelector(SELECTORS.PIPELINE_GRAPH)) { return; } @@ -36,6 +37,9 @@ const createLegacyPipelinesDetailApp = mediator => { mediator, }; }, + errorCaptured(err, _vm, info) { + reportToSentry('pipeline_details_bundle_legacy_details', `error: ${err}, info: ${info}`); + }, render(createElement) { return createElement('pipeline-graph-legacy', { props: { @@ -47,15 +51,15 @@ const createLegacyPipelinesDetailApp = mediator => { refreshPipelineGraph: this.requestRefreshPipelineGraph, onResetDownstream: (parentPipeline, pipeline) => this.resetDownstreamPipelines(parentPipeline, pipeline), - onClickUpstreamPipeline: pipeline => this.clickUpstreamPipeline(pipeline), - onClickDownstreamPipeline: pipeline => this.clickDownstreamPipeline(pipeline), + onClickUpstreamPipeline: (pipeline) => this.clickUpstreamPipeline(pipeline), + onClickDownstreamPipeline: (pipeline) => this.clickDownstreamPipeline(pipeline), }, }); }, }); }; -const createLegacyPipelineHeaderApp = mediator => { +const createLegacyPipelineHeaderApp = (mediator) => { if (!document.querySelector(SELECTORS.PIPELINE_HEADER)) { return; } @@ -78,6 +82,9 @@ const createLegacyPipelineHeaderApp = mediator => { eventHub.$off('headerPostAction', this.postAction); eventHub.$off('headerDeleteAction', this.deleteAction); }, + errorCaptured(err, _vm, info) { + reportToSentry('pipeline_details_bundle_legacy', `error: ${err}, info: ${info}`); + }, methods: { postAction(path) { this.mediator.service @@ -125,7 +132,7 @@ const createTestDetails = () => { }); }; -export default async function() { +export default async function () { createTestDetails(); createDagApp(); @@ -151,7 +158,7 @@ export default async function() { ); const { pipelineProjectPath, pipelineIid } = dataset; - createPipelinesDetailApp(SELECTORS.PIPELINE_DETAILS, pipelineProjectPath, pipelineIid); + createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, pipelineProjectPath, pipelineIid); } catch { Flash(__('An error occurred while loading the pipeline.')); } diff --git a/app/assets/javascripts/pipelines/pipeline_details_graph.js b/app/assets/javascripts/pipelines/pipeline_details_graph.js index 1b296c305cb..2d46bb5ec26 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_graph.js +++ b/app/assets/javascripts/pipelines/pipeline_details_graph.js @@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; import PipelineGraphWrapper from './components/graph/graph_component_wrapper.vue'; import { GRAPHQL } from './components/graph/constants'; +import { reportToSentry } from './components/graph/utils'; Vue.use(VueApollo); @@ -28,6 +29,9 @@ const createPipelinesDetailApp = (selector, pipelineProjectPath, pipelineIid) => pipelineIid, dataMethod: GRAPHQL, }, + errorCaptured(err, _vm, info) { + reportToSentry('pipeline_details_graph', `error: ${err}, info: ${info}`); + }, render(createElement) { return createElement(PipelineGraphWrapper); }, diff --git a/app/assets/javascripts/pipelines/pipeline_details_header.js b/app/assets/javascripts/pipelines/pipeline_details_header.js index 744a8272709..cba29acdb32 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_header.js +++ b/app/assets/javascripts/pipelines/pipeline_details_header.js @@ -9,7 +9,7 @@ const apolloProvider = new VueApollo({ defaultClient: createDefaultClient(), }); -export const createPipelineHeaderApp = elSelector => { +export const createPipelineHeaderApp = (elSelector) => { const el = document.querySelector(elSelector); if (!el) { diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediator.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js index d487970aed7..74c5fc45644 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_mediator.js +++ b/app/assets/javascripts/pipelines/pipeline_details_mediator.js @@ -55,7 +55,7 @@ export default class pipelinesMediator { return this.service .getPipeline() - .then(response => this.successCallback(response)) + .then((response) => this.successCallback(response)) .catch(() => this.errorCallback()) .finally(() => this.poll.restart( diff --git a/app/assets/javascripts/pipelines/stores/pipeline_store.js b/app/assets/javascripts/pipelines/stores/pipeline_store.js index c6f65277c8d..1f804a107a8 100644 --- a/app/assets/javascripts/pipelines/stores/pipeline_store.js +++ b/app/assets/javascripts/pipelines/stores/pipeline_store.js @@ -29,11 +29,11 @@ export default class PipelineStore { } if (pipelineCopy.triggered && pipelineCopy.triggered.length) { - pipelineCopy.triggered.forEach(el => { + pipelineCopy.triggered.forEach((el) => { const oldPipeline = this.state.pipeline && this.state.pipeline.triggered && - this.state.pipeline.triggered.find(element => element.id === el.id); + this.state.pipeline.triggered.find((element) => element.id === el.id); this.parseTriggeredPipelines(oldPipeline, el); }); @@ -67,8 +67,8 @@ export default class PipelineStore { } if (newPipeline.triggered_by?.length > 0) { - newPipeline.triggered_by.forEach(el => { - const oldTriggeredBy = oldPipeline.triggered_by?.find(element => element.id === el.id); + newPipeline.triggered_by.forEach((el) => { + const oldTriggeredBy = oldPipeline.triggered_by?.find((element) => element.id === el.id); this.parseTriggeredPipelines(oldTriggeredBy, el); }); } @@ -88,9 +88,9 @@ export default class PipelineStore { Vue.set(newPipeline, 'isLoading', false); if (newPipeline.triggered && newPipeline.triggered.length > 0) { - newPipeline.triggered.forEach(el => { + newPipeline.triggered.forEach((el) => { const oldTriggered = - oldPipeline.triggered && oldPipeline.triggered.find(element => element.id === el.id); + oldPipeline.triggered && oldPipeline.triggered.find((element) => element.id === el.id); this.parseTriggeredPipelines(oldTriggered, el); }); } @@ -102,7 +102,7 @@ export default class PipelineStore { * @param {Object} pipeline */ resetTriggeredByPipeline(parentPipeline, pipeline) { - parentPipeline.triggered_by.forEach(el => this.closePipeline(el)); + parentPipeline.triggered_by.forEach((el) => this.closePipeline(el)); if (pipeline.triggered_by && pipeline.triggered_by) { this.resetTriggeredByPipeline(pipeline, pipeline.triggered_by); @@ -129,7 +129,7 @@ export default class PipelineStore { this.closePipeline(pipeline); if (pipeline.triggered_by && pipeline.triggered_by.length) { - pipeline.triggered_by.forEach(triggeredBy => this.closeTriggeredByPipeline(triggeredBy)); + pipeline.triggered_by.forEach((triggeredBy) => this.closeTriggeredByPipeline(triggeredBy)); } } @@ -139,10 +139,10 @@ export default class PipelineStore { * @param {Object} pipeline */ resetTriggeredPipelines(parentPipeline, pipeline) { - parentPipeline.triggered.forEach(el => this.closePipeline(el)); + parentPipeline.triggered.forEach((el) => this.closePipeline(el)); if (pipeline.triggered && pipeline.triggered.length) { - pipeline.triggered.forEach(el => this.resetTriggeredPipelines(pipeline, el)); + pipeline.triggered.forEach((el) => this.resetTriggeredPipelines(pipeline, el)); } } @@ -165,7 +165,7 @@ export default class PipelineStore { this.closePipeline(pipeline); if (pipeline.triggered && pipeline.triggered.length) { - pipeline.triggered.forEach(triggered => this.closeTriggeredPipeline(triggered)); + pipeline.triggered.forEach((triggered) => this.closeTriggeredPipeline(triggered)); } } @@ -198,6 +198,9 @@ export default class PipelineStore { } removeExpandedPipelineToRequestData(id) { - this.state.expandedPipelines.splice(this.state.expandedPipelines.findIndex(el => el === id), 1); + this.state.expandedPipelines.splice( + this.state.expandedPipelines.findIndex((el) => el === id), + 1, + ); } } diff --git a/app/assets/javascripts/pipelines/stores/test_reports/getters.js b/app/assets/javascripts/pipelines/stores/test_reports/getters.js index 56f769c00fa..c31e7dd114f 100644 --- a/app/assets/javascripts/pipelines/stores/test_reports/getters.js +++ b/app/assets/javascripts/pipelines/stores/test_reports/getters.js @@ -1,18 +1,18 @@ import { addIconStatus, formattedTime } from './utils'; -export const getTestSuites = state => { +export const getTestSuites = (state) => { const { test_suites: testSuites = [] } = state.testReports; - return testSuites.map(suite => ({ + return testSuites.map((suite) => ({ ...suite, formattedTime: formattedTime(suite.total_time), })); }; -export const getSelectedSuite = state => +export const getSelectedSuite = (state) => state.testReports?.test_suites?.[state.selectedSuiteIndex] || {}; -export const getSuiteTests = state => { +export const getSuiteTests = (state) => { const { test_cases: testCases = [] } = getSelectedSuite(state); const { page, perPage } = state.pageInfo; const start = (page - 1) * perPage; @@ -20,4 +20,4 @@ export const getSuiteTests = state => { return testCases.map(addIconStatus).slice(start, start + perPage); }; -export const getSuiteTestCount = state => getSelectedSuite(state)?.test_cases?.length || 0; +export const getSuiteTestCount = (state) => getSelectedSuite(state)?.test_cases?.length || 0; diff --git a/app/assets/javascripts/pipelines/stores/test_reports/index.js b/app/assets/javascripts/pipelines/stores/test_reports/index.js index 88f61b09025..204dfc2fb01 100644 --- a/app/assets/javascripts/pipelines/stores/test_reports/index.js +++ b/app/assets/javascripts/pipelines/stores/test_reports/index.js @@ -7,7 +7,7 @@ import mutations from './mutations'; Vue.use(Vuex); -export default initialState => +export default (initialState) => new Vuex.Store({ actions, getters, diff --git a/app/assets/javascripts/pipelines/stores/test_reports/utils.js b/app/assets/javascripts/pipelines/stores/test_reports/utils.js index 42406e5a67a..5c1f27b166a 100644 --- a/app/assets/javascripts/pipelines/stores/test_reports/utils.js +++ b/app/assets/javascripts/pipelines/stores/test_reports/utils.js @@ -25,7 +25,7 @@ export const formattedTime = (seconds = 0) => { return sprintf(__('%{seconds}s'), { seconds: seconds.toFixed(2) }); }; -export const addIconStatus = testCase => ({ +export const addIconStatus = (testCase) => ({ ...testCase, icon: iconForTestStatus(testCase.status), formattedTime: formattedTime(testCase.execution_time), diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js index 28d6c0edb0f..50bb23b7e63 100644 --- a/app/assets/javascripts/pipelines/utils.js +++ b/app/assets/javascripts/pipelines/utils.js @@ -1,12 +1,11 @@ import { pickBy } from 'lodash'; import { SUPPORTED_FILTER_PARAMETERS } from './constants'; +import { createNodeDict } from './components/parsing_utils'; -export const validateParams = params => { +export const validateParams = (params) => { return pickBy(params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val); }; -export const createUniqueLinkId = (stageName, jobName) => `${stageName}-${jobName}`; - /** * This function takes the stages array and transform it * into a hash where each key is a job name and the job data @@ -15,19 +14,8 @@ export const createUniqueLinkId = (stageName, jobName) => `${stageName}-${jobNam * @returns {Object} - Hash of jobs */ export const createJobsHash = (stages = []) => { - const jobsHash = {}; - - stages.forEach(stage => { - if (stage.groups.length > 0) { - stage.groups.forEach(group => { - group.jobs.forEach(job => { - jobsHash[job.name] = job; - }); - }); - } - }); - - return jobsHash; + const nodes = stages.flatMap(({ groups }) => groups); + return createNodeDict(nodes); }; /** @@ -44,18 +32,26 @@ export const generateJobNeedsDict = (jobs = {}) => { const arrOfJobNames = Object.keys(jobs); return arrOfJobNames.reduce((acc, value) => { - const recursiveNeeds = jobName => { + const recursiveNeeds = (jobName) => { if (!jobs[jobName]?.needs) { return []; } return jobs[jobName].needs - .map(job => { + .map((job) => { // If we already have the needs of a job in the accumulator, // then we use the memoized data instead of the recursive call // to save some performance. const newNeeds = acc[job] ?? recursiveNeeds(job); + // In case it's a parallel job (size > 1), the name of the group + // and the job will be different. This mean we also need to add the group name + // to the list of `needs` to ensure we can properly reference it. + const group = jobs[job]; + if (group.size > 1) { + return [job, group.name, ...newNeeds]; + } + return [job, ...newNeeds]; }) .flat(Infinity); diff --git a/app/assets/javascripts/popovers/components/popovers.vue b/app/assets/javascripts/popovers/components/popovers.vue index 3bb6d284264..05a209a97ad 100644 --- a/app/assets/javascripts/popovers/components/popovers.vue +++ b/app/assets/javascripts/popovers/components/popovers.vue @@ -7,7 +7,7 @@ import { GlPopover } from '@gitlab/ui'; import { sanitize } from '~/lib/dompurify'; -const newPopover = element => { +const newPopover = (element) => { const { content, html, placement, title, triggers = 'focus' } = element.dataset; return { @@ -30,8 +30,8 @@ export default { }; }, created() { - this.observer = new MutationObserver(mutations => { - mutations.forEach(mutation => { + this.observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { mutation.removedNodes.forEach(this.dispose); }); }); @@ -61,7 +61,7 @@ export default { if (!target) { this.popovers = []; } else { - const index = this.popovers.findIndex(popover => popover.target === target); + const index = this.popovers.findIndex((popover) => popover.target === target); if (index > -1) { this.popovers.splice(index, 1); @@ -69,7 +69,7 @@ export default { } }, popoverExists(element) { - return this.popovers.some(popover => popover.target === element); + return this.popovers.some((popover) => popover.target === element); }, getSafeHtml(html) { return sanitize(html); diff --git a/app/assets/javascripts/popovers/index.js b/app/assets/javascripts/popovers/index.js index bfb61f02a3a..eebbfbdcc68 100644 --- a/app/assets/javascripts/popovers/index.js +++ b/app/assets/javascripts/popovers/index.js @@ -32,10 +32,10 @@ const handlePopoverEvent = (rootTarget, e, selector) => { }; export const initPopovers = () => { - ['mouseenter', 'focus', 'click'].forEach(event => { + ['mouseenter', 'focus', 'click'].forEach((event) => { document.addEventListener( event, - e => handlePopoverEvent(document, e, '[data-toggle="popover"]'), + (e) => handlePopoverEvent(document, e, '[data-toggle="popover"]'), true, ); }); @@ -43,7 +43,7 @@ export const initPopovers = () => { return getPopoversApp(); }; -export const dispose = elements => toArray(elements).forEach(getPopoversApp().dispose); +export const dispose = (elements) => toArray(elements).forEach(getPopoversApp().dispose); export const destroy = () => { getPopoversApp().$destroy(); diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue index 5feac7485ad..869fdccc800 100644 --- a/app/assets/javascripts/profile/account/components/update_username.vue +++ b/app/assets/javascripts/profile/account/components/update_username.vue @@ -84,12 +84,12 @@ Please update your Git repository remotes as soon as possible.`), return axios .put(this.actionUrl, putData) - .then(result => { + .then((result) => { Flash(result.data.message, 'notice'); this.username = username; this.isRequestPending = false; }) - .catch(error => { + .catch((error) => { Flash(error.response.data.message); this.isRequestPending = false; throw error; diff --git a/app/assets/javascripts/profile/add_ssh_key_validation.js b/app/assets/javascripts/profile/add_ssh_key_validation.js index ab6a6c1896c..5c78de7ffb0 100644 --- a/app/assets/javascripts/profile/add_ssh_key_validation.js +++ b/app/assets/javascripts/profile/add_ssh_key_validation.js @@ -12,7 +12,7 @@ export default class AddSshKeyValidation { } register() { - this.form.addEventListener('submit', event => this.submit(event)); + this.form.addEventListener('submit', (event) => this.submit(event)); this.confirmSubmitElement.addEventListener('click', () => { this.isValid = true; diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js index ecb69422287..afc78cbe78a 100644 --- a/app/assets/javascripts/profile/gl_crop.js +++ b/app/assets/javascripts/profile/gl_crop.js @@ -61,7 +61,7 @@ import { loadCSSFile } from '../lib/utils/css_utils'; bindEvents() { const _this = this; - this.fileInput.on('change', function(e) { + this.fileInput.on('change', function (e) { _this.onFileInputChange(e, this); this.value = null; }); @@ -69,7 +69,7 @@ import { loadCSSFile } from '../lib/utils/css_utils'; this.modalCrop.on('shown.bs.modal', this.onModalShow); this.modalCrop.on('hidden.bs.modal', this.onModalHide); this.uploadImageBtn.on('click', this.onUploadImageBtnClick); - this.cropActionsBtn.on('click', function() { + this.cropActionsBtn.on('click', function () { const btn = this; return _this.onActionBtnClick(btn); }); @@ -184,8 +184,8 @@ import { loadCSSFile } from '../lib/utils/css_utils'; const cropModal = document.querySelector('.modal-profile-crop'); if (cropModal) loadCSSFile(cropModal.dataset.cropperCssPath); - $.fn.glCrop = function(opts) { - return this.each(function() { + $.fn.glCrop = function (opts) { + return this.each(function () { return $(this).data('glcrop', new GitLabCrop(this, opts)); }); }; diff --git a/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js b/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js index bcca3140717..744e0174a4e 100644 --- a/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js +++ b/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js @@ -18,6 +18,6 @@ export default () => { el, name: 'ProfilePreferencesApp', provide, - render: createElement => createElement(ProfilePreferences), + render: (createElement) => createElement(ProfilePreferences), }); }; diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index 4755a4aa9ba..bfeeff47163 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -20,7 +20,7 @@ export default class Profile { this.timezoneDropdown = new TimezoneDropdown({ $inputEl: this.$inputEl, $dropdownEl: $('.js-timezone-dropdown'), - displayFormat: selectedItem => formatTimezone(selectedItem), + displayFormat: (selectedItem) => formatTimezone(selectedItem), }); } @@ -33,15 +33,13 @@ export default class Profile { uploadImageBtn: '.js-upload-user-avatar', modalCropImg: '.modal-profile-crop-image', }; - this.avatarGlCrop = $('.js-user-avatar-input') - .glCrop(cropOpts) - .data('glcrop'); + this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop'); } bindEvents() { $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm); $('.js-group-notification-email').on('change', this.submitForm); - $('#user_notification_email').on('select2-selecting', event => { + $('#user_notification_email').on('select2-selecting', (event) => { setTimeout(this.submitForm.bind(event.currentTarget)); }); $('#user_notified_of_own_activity').on('change', this.submitForm); @@ -91,7 +89,7 @@ export default class Profile { // Enable submit button after requests ends self.form.find(':input[disabled]').enable(); }) - .catch(error => flash(error.message)); + .catch((error) => flash(error.message)); } updateHeaderAvatar() { diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 0e12c219e45..ddb8956b664 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -10,7 +10,7 @@ import { deprecatedCreateFlash as flash } from '~/flash'; import { __ } from '~/locale'; // highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> ) -const highlighter = function(element, text, matches) { +const highlighter = function (element, text, matches) { let j = 0; let len = 0; let lastIndex = 0; @@ -57,7 +57,7 @@ export default class ProjectFindFile { initEvent() { // eslint-disable-next-line @gitlab/no-global-event-off this.inputElement.off('keyup'); - this.inputElement.on('keyup', event => { + this.inputElement.on('keyup', (event) => { const target = $(event.target); const value = target.val(); const ref = target.data('oldValue'); @@ -65,11 +65,7 @@ export default class ProjectFindFile { if (value !== oldValue) { target.data('oldValue', value); this.findFile(); - return this.element - .find('tr.tree-item') - .eq(0) - .addClass('selected') - .focus(); + return this.element.find('tr.tree-item').eq(0).addClass('selected').focus(); } }); } @@ -90,11 +86,7 @@ export default class ProjectFindFile { this.element.find('.loading').hide(); this.filePaths = data; this.findFile(); - this.element - .find('.files-slider tr.tree-item') - .eq(0) - .addClass('selected') - .focus(); + this.element.find('.files-slider tr.tree-item').eq(0).addClass('selected').focus(); }) .catch(() => flash(__('An error occurred while loading filenames'))); } diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index f7d823802b6..e68430d7dfd 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -9,7 +9,7 @@ import { loadCSSFile } from './lib/utils/css_utils'; const projectSelect = () => { loadCSSFile(gon.select2_css_path) .then(() => { - $('.ajax-project-select').each(function(i, select) { + $('.ajax-project-select').each(function (i, select) { let placeholder; const simpleFilter = $(select).data('simpleFilter') || false; const isInstantiated = $(select).data('select2'); @@ -33,17 +33,17 @@ const projectSelect = () => { $(select).select2({ placeholder, minimumInputLength: 0, - query: query => { + query: (query) => { let projectsCallback; - const finalCallback = function(projects) { + const finalCallback = function (projects) { const data = { results: projects, }; return query.callback(data); }; if (this.includeGroups) { - projectsCallback = function(projects) { - const groupsCallback = function(groups) { + projectsCallback = function (projects) { + const groupsCallback = function (groups) { const data = groups.concat(projects); return finalCallback(data); }; diff --git a/app/assets/javascripts/project_select_combo_button.js b/app/assets/javascripts/project_select_combo_button.js index 865dd23bd80..4b14df21f05 100644 --- a/app/assets/javascripts/project_select_combo_button.js +++ b/app/assets/javascripts/project_select_combo_button.js @@ -17,9 +17,9 @@ export default class ProjectSelectComboButton { bindEvents() { this.projectSelectInput .siblings('.new-project-item-select-button') - .on('click', e => this.openDropdown(e)); + .on('click', (e) => this.openDropdown(e)); - this.newItemBtn.on('click', e => { + this.newItemBtn.on('click', (e) => { if (!this.getProjectFromLocalStorage()) { e.preventDefault(); this.openDropdown(e); @@ -50,9 +50,7 @@ export default class ProjectSelectComboButton { // eslint-disable-next-line promise/no-nesting loadCSSFile(gon.select2_css_path) .then(() => { - $(event.currentTarget) - .siblings('.project-item-select') - .select2('open'); + $(event.currentTarget).siblings('.project-item-select').select2('open'); }) .catch(() => {}); }) @@ -104,14 +102,8 @@ export default class ProjectSelectComboButton { const defaultTextPrefix = this.resourceLabel; // the trailing slice call depluralizes each of these strings (e.g. new-issues -> new-issue) - const localStorageItemType = `new-${this.resourceType - .split('_') - .join('-') - .slice(0, -1)}`; - const presetTextSuffix = this.resourceType - .split('_') - .join(' ') - .slice(0, -1); + const localStorageItemType = `new-${this.resourceType.split('_').join('-').slice(0, -1)}`; + const presetTextSuffix = this.resourceType.split('_').join(' ').slice(0, -1); return { localStorageItemType, // new-issue / new-merge-request diff --git a/app/assets/javascripts/project_visibility.js b/app/assets/javascripts/project_visibility.js index aaf6723c85c..e3868e2925d 100644 --- a/app/assets/javascripts/project_visibility.js +++ b/app/assets/javascripts/project_visibility.js @@ -7,7 +7,7 @@ function setVisibilityOptions(namespaceSelector) { const selectedNamespace = namespaceSelector.options[namespaceSelector.selectedIndex]; const { name, visibility, visibilityLevel, showPath, editPath } = selectedNamespace.dataset; - document.querySelectorAll('.visibility-level-setting .form-check').forEach(option => { + document.querySelectorAll('.visibility-level-setting .form-check').forEach((option) => { const optionInput = option.querySelector('input[type=radio]'); const optionValue = optionInput ? optionInput.value : 0; const optionTitle = option.querySelector('.option-title'); diff --git a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue new file mode 100644 index 00000000000..3ecc3f1d1d3 --- /dev/null +++ b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue @@ -0,0 +1,94 @@ +<script> +import { + GlDropdown, + GlSearchBoxByType, + GlDropdownItem, + GlDropdownText, + GlLoadingIcon, +} from '@gitlab/ui'; +import { mapActions, mapGetters, mapState } from 'vuex'; +import { I18N_DROPDOWN } from '../constants'; + +export default { + name: 'BranchesDropdown', + components: { + GlDropdown, + GlSearchBoxByType, + GlDropdownItem, + GlDropdownText, + GlLoadingIcon, + }, + props: { + value: { + type: String, + required: false, + default: '', + }, + }, + i18n: I18N_DROPDOWN, + data() { + return { + searchTerm: this.value, + }; + }, + computed: { + ...mapGetters(['joinedBranches']), + ...mapState(['isFetching', 'branch', 'branches']), + filteredResults() { + const lowerCasedSearchTerm = this.searchTerm.toLowerCase(); + return this.joinedBranches.filter((resultString) => + resultString.toLowerCase().includes(lowerCasedSearchTerm), + ); + }, + }, + mounted() { + this.fetchBranches(this.searchTerm); + }, + methods: { + ...mapActions(['fetchBranches']), + selectBranch(branch) { + this.$emit('selectBranch', branch); + this.searchTerm = branch; // enables isSelected to work as expected + }, + isSelected(selectedBranch) { + return selectedBranch === this.branch; + }, + searchTermChanged(value) { + this.searchTerm = value; + this.fetchBranches(value); + }, + }, +}; +</script> +<template> + <gl-dropdown :text="value" :header-text="$options.i18n.headerTitle"> + <gl-search-box-by-type + :value="searchTerm" + trim + autocomplete="off" + :debounce="250" + :placeholder="$options.i18n.searchPlaceholder" + @input="searchTermChanged" + /> + <gl-dropdown-item + v-for="branch in filteredResults" + v-show="!isFetching" + :key="branch" + :name="branch" + :is-checked="isSelected(branch)" + is-check-item + @click="selectBranch(branch)" + > + {{ branch }} + </gl-dropdown-item> + <gl-dropdown-text v-show="isFetching" data-testid="dropdown-text-loading-icon"> + <gl-loading-icon class="gl-mx-auto" /> + </gl-dropdown-text> + <gl-dropdown-text + v-if="!filteredResults.length && !isFetching" + data-testid="empty-result-message" + > + <span class="gl-text-gray-500">{{ $options.i18n.noResultsMessage }}</span> + </gl-dropdown-text> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/projects/commit/components/form_modal.vue b/app/assets/javascripts/projects/commit/components/form_modal.vue new file mode 100644 index 00000000000..6411b1ca921 --- /dev/null +++ b/app/assets/javascripts/projects/commit/components/form_modal.vue @@ -0,0 +1,137 @@ +<script> +import { GlModal, GlForm, GlFormCheckbox, GlSprintf, GlFormGroup } from '@gitlab/ui'; +import { mapActions, mapState } from 'vuex'; +import eventHub from '../event_hub'; +import csrf from '~/lib/utils/csrf'; +import BranchesDropdown from './branches_dropdown.vue'; + +export default { + components: { + BranchesDropdown, + GlModal, + GlForm, + GlFormCheckbox, + GlSprintf, + GlFormGroup, + }, + inject: { + prependedText: { + default: '', + }, + }, + props: { + i18n: { + type: Object, + required: true, + }, + openModal: { + type: String, + required: true, + }, + modalId: { + type: String, + required: true, + }, + }, + data() { + return { + checked: true, + actionPrimary: { + text: this.i18n.actionPrimaryText, + attributes: [ + { variant: 'success' }, + { category: 'primary' }, + { 'data-testid': 'submit-commit' }, + ], + }, + actionCancel: { + text: this.i18n.actionCancelText, + attributes: [{ 'data-testid': 'cancel-commit' }], + }, + }; + }, + computed: { + ...mapState([ + 'branch', + 'endpoint', + 'pushCode', + 'branchCollaboration', + 'modalTitle', + 'existingBranch', + 'prependedText', + ]), + }, + mounted() { + eventHub.$on(this.openModal, this.show); + }, + methods: { + ...mapActions(['clearModal', 'setBranch', 'setSelectedBranch']), + show() { + this.$root.$emit('bv::show::modal', this.modalId); + }, + handlePrimary() { + this.$refs.form.$el.submit(); + }, + resetModalHandler() { + this.clearModal(); + this.setSelectedBranch(''); + this.checked = true; + }, + }, + csrf, +}; +</script> +<template> + <gl-modal + v-bind="$attrs" + data-testid="modal-commit" + :modal-id="modalId" + size="sm" + :title="modalTitle" + :action-cancel="actionCancel" + :action-primary="actionPrimary" + @hidden="resetModalHandler" + @primary="handlePrimary" + > + <p v-if="prependedText.length" data-testid="prepended-text"> + <gl-sprintf :message="prependedText" /> + </p> + + <gl-form ref="form" :action="endpoint" method="post"> + <input type="hidden" name="authenticity_token" :value="$options.csrf.token" /> + + <gl-form-group + :label="i18n.branchLabel" + label-for="start_branch" + data-testid="dropdown-group" + > + <input id="start_branch" type="hidden" name="start_branch" :value="branch" /> + + <branches-dropdown class="gl-w-half" :value="branch" @selectBranch="setBranch" /> + </gl-form-group> + + <gl-form-checkbox + v-if="pushCode" + v-model="checked" + name="create_merge_request" + class="gl-mt-3" + > + <gl-sprintf :message="i18n.startMergeRequest"> + <template #newMergeRequest> + <strong>{{ i18n.newMergeRequest }}</strong> + </template> + </gl-sprintf> + </gl-form-checkbox> + <input v-else type="hidden" name="create_merge_request" value="1" /> + </gl-form> + + <p v-if="!pushCode" class="gl-mb-0 gl-mt-5" data-testid="appended-text"> + <gl-sprintf v-if="branchCollaboration" :message="i18n.existingBranch"> + <template #branchName> + <strong>{{ existingBranch }}</strong> + </template> + </gl-sprintf> + <gl-sprintf v-else :message="i18n.branchInFork" /> + </p> + </gl-modal> +</template> diff --git a/app/assets/javascripts/projects/commit/components/form_trigger.vue b/app/assets/javascripts/projects/commit/components/form_trigger.vue new file mode 100644 index 00000000000..e92854c1ac3 --- /dev/null +++ b/app/assets/javascripts/projects/commit/components/form_trigger.vue @@ -0,0 +1,32 @@ +<script> +import { GlLink } from '@gitlab/ui'; +import eventHub from '../event_hub'; + +export default { + components: { + GlLink, + }, + inject: { + displayText: { + default: '', + }, + }, + props: { + openModal: { + type: String, + required: true, + }, + }, + methods: { + showModal() { + eventHub.$emit(this.openModal); + }, + }, +}; +</script> + +<template> + <gl-link data-is-link="true" data-testid="revert-commit-link" @click="showModal"> + {{ displayText }} + </gl-link> +</template> diff --git a/app/assets/javascripts/projects/commit/constants.js b/app/assets/javascripts/projects/commit/constants.js new file mode 100644 index 00000000000..233f43d56b9 --- /dev/null +++ b/app/assets/javascripts/projects/commit/constants.js @@ -0,0 +1,33 @@ +import { s__, __ } from '~/locale'; + +export const OPEN_REVERT_MODAL = 'openRevertModal'; +export const REVERT_MODAL_ID = 'revert-commit-modal'; + +export const I18N_MODAL = { + startMergeRequest: s__('ChangeTypeAction|Start a %{newMergeRequest} with these changes'), + existingBranch: s__( + 'ChangeTypeAction|Your changes will be committed to %{branchName} because a merge request is open.', + ), + branchInFork: s__( + 'ChangeTypeAction|A new branch will be created in your fork and a new merge request will be started.', + ), + newMergeRequest: __('new merge request'), + actionCancelText: __('Cancel'), +}; + +export const I18N_REVERT_MODAL = { + branchLabel: s__('ChangeTypeAction|Revert in branch'), + actionPrimaryText: s__('ChangeTypeAction|Revert'), +}; + +export const PREPENDED_MODAL_TEXT = s__( + 'ChangeTypeAction|This will create a new commit in order to revert the existing changes.', +); + +export const I18N_DROPDOWN = { + noResultsMessage: __('No matching results'), + headerTitle: s__('ChangeTypeAction|Switch branch'), + searchPlaceholder: s__('ChangeTypeAction|Search branches'), +}; + +export const PROJECT_BRANCHES_ERROR = __('Something went wrong while fetching branches'); diff --git a/app/assets/javascripts/projects/commit/event_hub.js b/app/assets/javascripts/projects/commit/event_hub.js new file mode 100644 index 00000000000..e31806ad199 --- /dev/null +++ b/app/assets/javascripts/projects/commit/event_hub.js @@ -0,0 +1,3 @@ +import createEventHub from '~/helpers/event_hub_factory'; + +export default createEventHub(); diff --git a/app/assets/javascripts/projects/commit/init_revert_commit_modal.js b/app/assets/javascripts/projects/commit/init_revert_commit_modal.js new file mode 100644 index 00000000000..ec0600cd25a --- /dev/null +++ b/app/assets/javascripts/projects/commit/init_revert_commit_modal.js @@ -0,0 +1,55 @@ +import Vue from 'vue'; +import CommitFormModal from './components/form_modal.vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import createStore from './store'; +import { + I18N_MODAL, + I18N_REVERT_MODAL, + PREPENDED_MODAL_TEXT, + OPEN_REVERT_MODAL, + REVERT_MODAL_ID, +} from './constants'; + +export default function initInviteMembersModal() { + const el = document.querySelector('.js-revert-commit-modal'); + if (!el) { + return false; + } + + const { + title, + endpoint, + branch, + pushCode, + branchCollaboration, + existingBranch, + branchesEndpoint, + } = el.dataset; + + const store = createStore({ + endpoint, + branchesEndpoint, + branch, + pushCode: parseBoolean(pushCode), + branchCollaboration: parseBoolean(branchCollaboration), + defaultBranch: branch, + modalTitle: title, + existingBranch, + }); + + return new Vue({ + el, + store, + provide: { + prependedText: PREPENDED_MODAL_TEXT, + }, + render: (createElement) => + createElement(CommitFormModal, { + props: { + i18n: { ...I18N_REVERT_MODAL, ...I18N_MODAL }, + openModal: OPEN_REVERT_MODAL, + modalId: REVERT_MODAL_ID, + }, + }), + }); +} diff --git a/app/assets/javascripts/projects/commit/init_revert_commit_trigger.js b/app/assets/javascripts/projects/commit/init_revert_commit_trigger.js new file mode 100644 index 00000000000..0bb57f22663 --- /dev/null +++ b/app/assets/javascripts/projects/commit/init_revert_commit_trigger.js @@ -0,0 +1,20 @@ +import Vue from 'vue'; +import RevertCommitTrigger from './components/form_trigger.vue'; +import { OPEN_REVERT_MODAL } from './constants'; + +export default function initInviteMembersTrigger() { + const el = document.querySelector('.js-revert-commit-trigger'); + + if (!el) { + return false; + } + + const { displayText } = el.dataset; + + return new Vue({ + el, + provide: { displayText }, + render: (createElement) => + createElement(RevertCommitTrigger, { props: { openModal: OPEN_REVERT_MODAL } }), + }); +} diff --git a/app/assets/javascripts/projects/commit/store/actions.js b/app/assets/javascripts/projects/commit/store/actions.js new file mode 100644 index 00000000000..2ae0370d579 --- /dev/null +++ b/app/assets/javascripts/projects/commit/store/actions.js @@ -0,0 +1,36 @@ +import * as types from './mutation_types'; +import axios from '~/lib/utils/axios_utils'; +import createFlash from '~/flash'; +import { PROJECT_BRANCHES_ERROR } from '../constants'; + +export const clearModal = ({ commit }) => { + commit(types.CLEAR_MODAL); +}; + +export const requestBranches = ({ commit }) => { + commit(types.REQUEST_BRANCHES); +}; + +export const fetchBranches = ({ commit, dispatch, state }, query) => { + dispatch('requestBranches'); + + return axios + .get(state.branchesEndpoint, { + params: { search: query }, + }) + .then((res) => { + commit(types.RECEIVE_BRANCHES_SUCCESS, res.data); + }) + .catch(() => { + createFlash({ message: PROJECT_BRANCHES_ERROR }); + }); +}; + +export const setBranch = ({ commit, dispatch }, branch) => { + commit(types.SET_BRANCH, branch); + dispatch('setSelectedBranch', branch); +}; + +export const setSelectedBranch = ({ commit }, branch) => { + commit(types.SET_SELECTED_BRANCH, branch); +}; diff --git a/app/assets/javascripts/projects/commit/store/getters.js b/app/assets/javascripts/projects/commit/store/getters.js new file mode 100644 index 00000000000..664eaca32cf --- /dev/null +++ b/app/assets/javascripts/projects/commit/store/getters.js @@ -0,0 +1,5 @@ +import { uniq } from 'lodash'; + +export const joinedBranches = (state) => { + return uniq(state.branches).sort(); +}; diff --git a/app/assets/javascripts/projects/commit/store/index.js b/app/assets/javascripts/projects/commit/store/index.js new file mode 100644 index 00000000000..83802f6a36f --- /dev/null +++ b/app/assets/javascripts/projects/commit/store/index.js @@ -0,0 +1,19 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; +import state from './state'; + +Vue.use(Vuex); + +export default (initialState = {}) => + new Vuex.Store({ + actions, + mutations, + getters, + state: { + ...state(), + ...initialState, + }, + }); diff --git a/app/assets/javascripts/projects/commit/store/mutation_types.js b/app/assets/javascripts/projects/commit/store/mutation_types.js new file mode 100644 index 00000000000..de0bb47e18d --- /dev/null +++ b/app/assets/javascripts/projects/commit/store/mutation_types.js @@ -0,0 +1,6 @@ +export const CLEAR_MODAL = 'CLEAR_MODAL'; + +export const REQUEST_BRANCHES = 'REQUEST_BRANCHES'; +export const RECEIVE_BRANCHES_SUCCESS = 'RECEIVE_BRANCHES_SUCCESS'; +export const SET_BRANCH = 'SET_BRANCH'; +export const SET_SELECTED_BRANCH = 'SET_SELECTED_BRANCH'; diff --git a/app/assets/javascripts/projects/commit/store/mutations.js b/app/assets/javascripts/projects/commit/store/mutations.js new file mode 100644 index 00000000000..6add00deadb --- /dev/null +++ b/app/assets/javascripts/projects/commit/store/mutations.js @@ -0,0 +1,25 @@ +import * as types from './mutation_types'; + +export default { + [types.REQUEST_BRANCHES](state) { + state.isFetching = true; + }, + + [types.RECEIVE_BRANCHES_SUCCESS](state, branches) { + state.isFetching = false; + state.branches = branches; + state.branches.unshift(state.branch); + }, + + [types.CLEAR_MODAL](state) { + state.branch = state.defaultBranch; + }, + + [types.SET_BRANCH](state, branch) { + state.branch = branch; + }, + + [types.SET_SELECTED_BRANCH](state, branch) { + state.selectedBranch = branch; + }, +}; diff --git a/app/assets/javascripts/projects/commit/store/state.js b/app/assets/javascripts/projects/commit/store/state.js new file mode 100644 index 00000000000..78c294324df --- /dev/null +++ b/app/assets/javascripts/projects/commit/store/state.js @@ -0,0 +1,13 @@ +export default () => ({ + endpoint: null, + branchesEndpoint: null, + isFetching: false, + branches: [], + selectedBranch: '', + pushCode: false, + branchCollaboration: false, + modalTitle: '', + existingBranch: '', + defaultBranch: '', + branch: '', +}); diff --git a/app/assets/javascripts/projects/commit_box/info/init_details_button.js b/app/assets/javascripts/projects/commit_box/info/init_details_button.js index 032fbf5316a..833e946af5c 100644 --- a/app/assets/javascripts/projects/commit_box/info/init_details_button.js +++ b/app/assets/javascripts/projects/commit_box/info/init_details_button.js @@ -3,9 +3,7 @@ import $ from 'jquery'; export const initDetailsButton = () => { $('body').on('click', '.js-details-expand', function expand(e) { e.preventDefault(); - $(this) - .next('.js-details-content') - .removeClass('hide'); + $(this).next('.js-details-content').removeClass('hide'); $(this).hide(); }); }; diff --git a/app/assets/javascripts/projects/commit_box/info/load_branches.js b/app/assets/javascripts/projects/commit_box/info/load_branches.js index 0efa1998507..8a0b2c30abe 100644 --- a/app/assets/javascripts/projects/commit_box/info/load_branches.js +++ b/app/assets/javascripts/projects/commit_box/info/load_branches.js @@ -2,7 +2,7 @@ import axios from 'axios'; import { sanitize } from '~/lib/dompurify'; import { __ } from '~/locale'; -export const loadBranches = containerEl => { +export const loadBranches = (containerEl) => { if (!containerEl) { return; } diff --git a/app/assets/javascripts/projects/commits/components/author_select.vue b/app/assets/javascripts/projects/commits/components/author_select.vue index 3bc772fe60a..752bb594794 100644 --- a/app/assets/javascripts/projects/commits/components/author_select.vue +++ b/app/assets/javascripts/projects/commits/components/author_select.vue @@ -69,7 +69,7 @@ export default { commitsSearchInput.addEventListener( 'keyup', - debounce(event => this.setSearchParam(event.target.value), 500), // keyup & time is to match effect of "filter by commit message" + debounce((event) => this.setSearchParam(event.target.value), 500), // keyup & time is to match effect of "filter by commit message" ); }, methods: { diff --git a/app/assets/javascripts/projects/commits/index.js b/app/assets/javascripts/projects/commits/index.js index 6f85432a77e..03b94fde0f3 100644 --- a/app/assets/javascripts/projects/commits/index.js +++ b/app/assets/javascripts/projects/commits/index.js @@ -5,7 +5,7 @@ import store from './store'; Vue.use(Vuex); -export default el => { +export default (el) => { if (!el) { return null; } diff --git a/app/assets/javascripts/projects/commits/store/actions.js b/app/assets/javascripts/projects/commits/store/actions.js index 157e2409f7f..359d81f32f7 100644 --- a/app/assets/javascripts/projects/commits/store/actions.js +++ b/app/assets/javascripts/projects/commits/store/actions.js @@ -26,7 +26,7 @@ export default { }, }) .then(({ data }) => dispatch('receiveAuthorsSuccess', data)) - .catch(error => { + .catch((error) => { Sentry.captureException(error); dispatch('receiveAuthorsError'); }); diff --git a/app/assets/javascripts/projects/default_project_templates.js b/app/assets/javascripts/projects/default_project_templates.js index bc3b29cde0a..2da9449d24e 100644 --- a/app/assets/javascripts/projects/default_project_templates.js +++ b/app/assets/javascripts/projects/default_project_templates.js @@ -97,4 +97,8 @@ export default { text: s__('ProjectTemplates|GitLab Cluster Management'), icon: '.template-option .icon-cluster_management', }, + kotlin_native_linux: { + text: s__('ProjectTemplates|Kotlin Native for Linux'), + icon: '.template-option .icon-gitlab_logo', + }, }; diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue b/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue index 2e16071e563..b54f7051806 100644 --- a/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue +++ b/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue @@ -1,6 +1,6 @@ <script> /* eslint-disable vue/no-v-html */ -import { GlBreadcrumb, GlIcon } from '@gitlab/ui'; +import { GlBreadcrumb, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import WelcomePage from './welcome.vue'; import LegacyContainer from './legacy_container.vue'; import { __, s__ } from '~/locale'; @@ -57,7 +57,9 @@ export default { WelcomePage, LegacyContainer, }, - + directives: { + SafeHtml, + }, props: { hasErrors: { type: Boolean, @@ -69,6 +71,11 @@ export default { required: false, default: false, }, + newProjectGuidelines: { + type: String, + required: false, + default: '', + }, }, data() { @@ -83,11 +90,11 @@ export default { return PANELS; } - return PANELS.filter(p => p.name !== CI_CD_PANEL); + return PANELS.filter((p) => p.name !== CI_CD_PANEL); }, activePanel() { - return PANELS.find(p => p.name === this.activeTab); + return PANELS.find((p) => p.name === this.activeTab); }, breadcrumbs() { @@ -113,7 +120,7 @@ export default { this.handleLocationHashChange(); this.resetProjectErrors(); }); - this.$root.$on('clicked::link', e => { + this.$root.$on('clicked::link', (e) => { window.location = e.target.href; }); }, @@ -142,9 +149,14 @@ export default { <welcome-page v-if="activeTab === null" :panels="availablePanels" /> <div v-else class="row"> <div class="col-lg-3"> - <div class="text-center" v-html="activePanel.illustration"></div> + <div class="gl-text-white" v-html="activePanel.illustration"></div> <h4>{{ activePanel.title }}</h4> <p>{{ activePanel.description }}</p> + <div + v-if="newProjectGuidelines" + id="new-project-guideline" + v-safe-html="newProjectGuidelines" + ></div> </div> <div class="col-lg-9"> <gl-breadcrumb v-if="breadcrumbs" :items="breadcrumbs"> diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue b/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue index 022328cd8a2..63a65975fff 100644 --- a/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue +++ b/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue @@ -37,7 +37,7 @@ export default { class="blank-state blank-state-link experiment-new-project-page-blank-state" @click="track('click_tab', { label: panel.name })" > - <div class="blank-state-icon" v-html="panel.illustration"></div> + <div class="blank-state-icon gl-text-white" v-html="panel.illustration"></div> <div class="blank-state-body gl-pl-4!"> <h3 class="blank-state-title experiment-new-project-page-blank-state-title"> {{ panel.title }} diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/blank-project.svg b/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/blank-project.svg index 0d8021658d1..f73ae70dba8 100644 --- a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/blank-project.svg +++ b/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/blank-project.svg @@ -1,27 +1,9 @@ -<?xml version="1.0" encoding="UTF-8"?> -<svg width="215px" height="115px" viewBox="0 0 215 115" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <!-- Generator: Sketch 61.2 (89653) - https://sketch.com --> - <title>create-new-project-md</title> - <desc>Created with Sketch.</desc> - <g id="create-new-project-md" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> - <g id="Group-3" transform="translate(71.000000, 18.000000)" fill-rule="nonzero"> - <g id="New-Blank1"> - <path d="M6.11141667,3.90697674 L62.6947849,3.90697674 C65.9485064,3.90697674 68.5891473,6.56494969 68.5891473,9.8400273 L68.5891473,78.0669494 C68.5891473,81.342027 65.9485064,84 62.6947849,84 L6.11141667,84 C2.85769514,84 0.217054264,81.342027 0.217054264,78.0669494 L0.217054264,9.8400273 C0.217054264,6.56494969 2.85769514,3.90697674 6.11141667,3.90697674 Z" id="Path" fill="#F9F9F9"></path> - <path d="M8.89436241,1 L65.4777306,1 C68.7314521,1 71.372093,3.65598929 71.372093,6.9286227 L71.372093,74.5132378 C71.372093,77.7858712 68.7314521,80.4418605 65.4777306,80.4418605 L8.89436241,80.4418605 C5.64064088,80.4418605 3,77.7858712 3,74.5132378 L3,6.9286227 C3.00209243,3.65598929 5.64064088,1 8.89436241,1 Z" id="Path" fill="#FFFFFF"></path> - <path d="M9.2677971,2.35980136 C6.65357171,2.35980136 4.53489427,4.47043114 4.53489427,7.07940407 L4.53489427,74.3201325 C4.53489427,76.9270116 6.65147193,79.0397352 9.2677971,79.0397352 L66.0500324,79.0397352 C68.6642577,79.0397352 70.7829352,76.9291055 70.7829352,74.3201325 L70.7829352,7.07731019 C70.7829352,4.47043114 68.6663575,2.35770748 66.0500324,2.35770748 L9.2677971,2.35980136 L9.2677971,2.35980136 Z M9.2677971,0 L66.0500324,0 C69.9724203,0 73.1472868,3.16803856 73.1472868,7.07731019 L73.1472868,74.3180386 C73.1472868,78.2294042 69.9703205,81.3953488 66.0500324,81.3953488 L9.2677971,81.3953488 C5.34540913,81.3953488 2.17054264,78.2273103 2.17054264,74.3180386 L2.17054264,7.07731019 C2.17054264,3.17222631 5.34750891,0 9.2677971,0 Z" id="Shape" fill="#EEEEEE"></path> - <path d="M21.6234891,28.6511628 L28.9501543,28.6511628 C29.6221266,28.6511628 30.1705426,29.2387129 30.1705426,29.9534884 C30.1705426,30.6682639 29.6199589,31.255814 28.9501543,31.255814 L21.6234891,31.255814 C20.9515168,31.255814 20.4031008,30.6682639 20.4031008,29.9534884 C20.4031008,29.2387129 20.9515168,28.6511628 21.6234891,28.6511628 Z" id="Path" fill="#E1DBF1"></path> - <path d="M33.9142229,35.8139535 L36.1943042,35.8139535 C36.8214783,35.8139535 37.3333333,36.4015036 37.3333333,37.1162791 C37.3333333,37.8333678 36.8194552,38.4186047 36.1943042,38.4186047 L33.9142229,38.4186047 C33.2870488,38.4186047 32.7751938,37.8310546 32.7751938,37.1162791 C32.7751938,36.4015036 33.2890719,35.8139535 33.9142229,35.8139535 Z" id="Path" fill="#FC6D26"></path> - <path d="M24.200844,42.9767442 L28.9774506,42.9767442 C29.6343929,42.9767442 30.1705426,43.5642943 30.1705426,44.2790698 C30.1705426,44.9961585 29.6322737,45.5813953 28.9774506,45.5813953 L24.200844,45.5813953 C23.5439017,45.5813953 23.0077519,44.9938453 23.0077519,44.2790698 C23.0077519,43.5642943 23.5439017,42.9767442 24.200844,42.9767442 Z" id="Path" fill="#E1DBF1"></path> - <path d="M41.0770181,35.8139535 L43.3570964,35.8139535 C43.9842697,35.8139535 44.496124,36.4015036 44.496124,37.1162791 C44.496124,37.8333678 43.9822466,38.4186047 43.3570964,38.4186047 L41.0770181,38.4186047 C40.4498448,38.4186047 39.9379845,37.8310546 39.9379845,37.1162791 C39.9359673,36.4015036 40.4498448,35.8139535 41.0770181,35.8139535 Z" id="Path" fill="#FC6D26"></path> - <path d="M33.9372473,28.6511628 L47.89221,28.6511628 C48.5320619,28.6511628 49.0542636,29.2387129 49.0542636,29.9534884 C49.0542636,30.6682639 48.5299978,31.255814 47.89221,31.255814 L33.9372473,31.255814 C33.2973955,31.255814 32.7751938,30.6682639 32.7751938,29.9534884 C32.7751938,29.2387129 33.2994595,28.6511628 33.9372473,28.6511628 Z" id="Path" fill="#C3B8E3"></path> - <path d="M33.9142229,42.9767442 L36.1943042,42.9767442 C36.8214783,42.9767442 37.3333333,43.5642943 37.3333333,44.2790698 C37.3333333,44.9961585 36.8194552,45.5813953 36.1943042,45.5813953 L33.9142229,45.5813953 C33.2870488,45.5813953 32.7751938,44.9938453 32.7751938,44.2790698 C32.7751938,43.5642943 33.2890719,42.9767442 33.9142229,42.9767442 Z" id="Path" fill="#6B4FBB"></path> - <g id="Group" transform="translate(16.000000, 19.000000)"> - <circle id="Oval" fill="#FFFFFF" cx="20.8396947" cy="20.8396947" r="20.7533889"></circle> - <path d="M20.8396947,41.5930835 C9.3778626,41.5930835 0.0863058062,32.3015267 0.0863058062,20.8396947 C0.0863058062,9.3778626 9.3778626,0.0863058062 20.8396947,0.0863058062 C32.3015267,0.0863058062 41.5930835,9.3778626 41.5930835,20.8396947 C41.5930835,32.3015267 32.3015267,41.5930835 20.8396947,41.5930835 Z M20.8396947,39.2207263 C30.9922045,39.2207263 39.2207263,30.9900995 39.2207263,20.8396947 C39.2207263,10.6892898 30.9900995,2.45866297 20.8396947,2.45866297 C10.6892898,2.45866297 2.45866297,10.6892898 2.45866297,20.8396947 C2.45866297,30.9900995 10.6871848,39.2207263 20.8396947,39.2207263 Z" id="Shape" fill="#EEEEEE"></path> - <path d="M13.7647236,19.060953 L27.9967615,19.060953 C28.6493176,19.060953 29.1818876,19.595628 29.1818876,20.2460791 C29.1818876,20.8986352 28.6472126,21.4312052 27.9967615,21.4312052 L13.7647236,21.4312052 C13.1121675,21.4312052 12.5795975,20.8965302 12.5795975,20.2460791 C12.5795975,19.593523 13.1142725,19.060953 13.7647236,19.060953 Z" id="Path" fill="#6B4FBB"></path> - <path d="M22.0669211,13.1311127 L22.0669211,27.3631506 C22.0669211,28.0157067 21.5322461,28.5482767 20.881795,28.5482767 C20.231344,28.5482767 19.696669,28.0136017 19.696669,27.3631506 L19.696669,13.1311127 C19.696669,12.4785566 20.231344,11.9459866 20.881795,11.9459866 C21.5322461,11.9459866 22.0669211,12.4785566 22.0669211,13.1311127 Z" id="Path" fill="#6B4FBB"></path> - </g> - </g> - </g> - </g> -</svg>
\ No newline at end of file +<svg width="82" height="80" viewBox="0 0 82 80" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M66.1912 8.19118H77.6176C78.2755 8.19118 78.8088 8.72448 78.8088 9.38235V69.6176C78.8088 70.2755 78.2755 70.8088 77.6176 70.8088H66.1912V8.19118Z" fill="#F0F0F0" stroke="#DBDBDB" stroke-width="2.38235"/> +<path d="M22.0517 19.2723L22.0094 10.1001C22.004 8.92546 22.8555 7.92221 24.0153 7.73664L63.3613 1.44139C64.8087 1.2098 66.12 2.32794 66.12 3.79382V75.8717C66.12 77.3323 64.8177 78.449 63.3742 78.2262L24.3037 72.1952C23.1461 72.0165 22.2902 71.023 22.2848 69.8517L22.2428 60.7554" stroke="#DBDBDB" stroke-width="2.38235"/> +<circle cx="23" cy="40" r="21" stroke="#6E49CB" stroke-width="2.38235"/> +<circle cx="23" cy="40" r="17" fill="#6E49CB"/> +<circle cx="23" cy="40" r="17" fill="white" fill-opacity="0.9"/> +<path d="M22.3125 48V33.3659" stroke="#6E49CB" stroke-width="2.38235" stroke-linecap="round"/> +<path d="M15 40.3049H30" stroke="#6E49CB" stroke-width="2.38235" stroke-linecap="round"/> +</svg> diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/ci-cd-project.svg b/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/ci-cd-project.svg index c85e1a245b8..8d6cf58f196 100644 --- a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/ci-cd-project.svg +++ b/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/ci-cd-project.svg @@ -1,73 +1,23 @@ -<?xml version="1.0" encoding="UTF-8"?> -<svg width="215px" height="115px" viewBox="0 0 215 115" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <!-- Generator: Sketch 61.2 (89653) - https://sketch.com --> - <title>run-CICD-pipelines-md</title> - <desc>Created with Sketch.</desc> - <g id="run-CICD-pipelines-md" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> - <g id="CICD-Repoj1" transform="translate(22.000000, 16.000000)"> - <g id="Group" transform="translate(100.000000, 0.000000)"> - <path d="M5.88230137,4.144 L61.8884384,4.144 C65.1195616,4.144 67.7155068,6.804 67.7155068,10.052 L67.7155068,78.064 C67.7155068,81.34 65.0919452,83.972 61.8884384,83.972 L5.88230137,83.972 C2.65117808,83.972 0.0552328767,81.312 0.0552328767,78.064 L0.0552328767,10.052 C0.0552328767,6.804 2.67879452,4.144 5.88230137,4.144 Z" id="Path" fill="#F9F9F9" fill-rule="nonzero"></path> - <path d="M8.80964384,1.176 L64.8157808,1.176 C68.0469041,1.176 70.6428493,3.836 70.6428493,7.084 L70.6428493,74.508 C70.6428493,77.784 68.0192877,80.416 64.8157808,80.416 L8.80964384,80.416 C5.57852055,80.416 2.98257534,77.756 2.98257534,74.508 L2.98257534,7.112 C2.98257534,3.836 5.57852055,1.176 8.80964384,1.176 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path> - <path d="M8.80964384,2.38 C6.24131507,2.38 4.14246575,4.508 4.14246575,7.112 L4.14246575,74.536 C4.14246575,77.14 6.24131507,79.268 8.80964384,79.268 L64.8157808,79.268 C67.3841096,79.268 69.4829589,77.14 69.4829589,74.536 L69.4829589,7.112 C69.4829589,4.508 67.3841096,2.38 64.8157808,2.38 L8.80964384,2.38 L8.80964384,2.38 Z M8.80964384,0 L64.8157808,0 C68.6820822,0 71.8027397,3.164 71.8027397,7.084 L71.8027397,74.508 C71.8027397,78.428 68.6820822,81.592 64.8157808,81.592 L8.80964384,81.592 C4.94334247,81.592 1.82250462,78.4 1.82250462,74.508 L1.82250462,7.112 C1.79506849,3.192 4.94334247,0 8.80964384,0 Z" id="Shape" fill="#EEEEEE" fill-rule="nonzero"></path> - <path d="M14.6367123,14.784 L21.6236712,14.784 C22.2588493,14.784 22.7835616,15.316 22.7835616,15.96 C22.7835616,16.604 22.2588493,17.136 21.6236712,17.136 L14.6367123,17.136 C14.0015342,17.136 13.4768219,16.604 13.4768219,15.96 C13.4768219,15.316 14.0015342,14.784 14.6367123,14.784 Z M33.3054247,21.896 L40.2923836,21.896 C40.9275616,21.896 41.452274,22.428 41.452274,23.072 C41.452274,23.716 40.9275616,24.248 40.2923836,24.248 L33.3054247,24.248 C32.6702466,24.248 32.1455342,23.716 32.1455342,23.072 C32.1455342,22.428 32.6702466,21.896 33.3054247,21.896 Z" id="Shape" fill="#E1DBF1" fill-rule="nonzero"></path> - <path d="M40.32,14.784 L47.3069589,14.784 C47.942137,14.784 48.4668493,15.316 48.4668493,15.96 C48.4668493,16.604 47.942137,17.136 47.3069589,17.136 L40.32,17.136 C39.6848219,17.136 39.1601096,16.604 39.1601096,15.96 C39.1324932,15.316 39.6572055,14.784 40.32,14.784 Z" id="Path" fill="#EEEEEE" fill-rule="nonzero"></path> - <path d="M21.6512877,28.98 L28.6382466,28.98 C29.2734247,28.98 29.798137,29.512 29.798137,30.156 C29.798137,30.8 29.2734247,31.332 28.6382466,31.332 L21.6512877,31.332 C21.0161096,31.332 20.4913973,30.8 20.4913973,30.156 C20.4637808,29.512 20.9884932,28.98 21.6512877,28.98 Z" id="Path" fill="#E1DBF1" fill-rule="nonzero"></path> - <path d="M26.2908493,14.784 L28.6382466,14.784 C29.2734247,14.784 29.798137,15.316 29.798137,15.96 C29.798137,16.604 29.2734247,17.136 28.6382466,17.136 L26.2908493,17.136 C25.6556712,17.136 25.1309589,16.604 25.1309589,15.96 C25.1309589,15.316 25.6556712,14.784 26.2908493,14.784 Z" id="Path" fill="#FEE1D3" fill-rule="nonzero"></path> - <path d="M33.3054247,36.092 L35.6528219,36.092 C36.288,36.092 36.8127123,36.624 36.8127123,37.268 C36.8127123,37.912 36.288,38.444 35.6528219,38.444 L33.3054247,38.444 C32.6702466,38.444 32.1455342,37.912 32.1455342,37.268 C32.1455342,36.624 32.6702466,36.092 33.3054247,36.092 Z" id="Path" fill="#FC6D26" fill-rule="nonzero"></path> - <path d="M44.9595616,21.896 L47.3069589,21.896 C47.942137,21.896 48.4668493,22.428 48.4668493,23.072 C48.4668493,23.716 47.942137,24.248 47.3069589,24.248 L44.9595616,24.248 C44.3243836,24.248 43.7996712,23.716 43.7996712,23.072 C43.7996712,22.428 44.3243836,21.896 44.9595616,21.896 Z M51.974137,14.784 L54.3215342,14.784 C54.9567123,14.784 55.4814247,15.316 55.4814247,15.96 C55.4814247,16.604 54.9567123,17.136 54.3215342,17.136 L51.974137,17.136 C51.3389589,17.136 50.8142466,16.604 50.8142466,15.96 C50.8142466,15.316 51.3389589,14.784 51.974137,14.784 Z" id="Shape" fill="#FEF0E8" fill-rule="nonzero"></path> - <path d="M23.9710685,43.176 L28.6382466,43.176 C29.2734247,43.176 29.798137,43.708 29.798137,44.352 C29.798137,44.996 29.2734247,45.528 28.6382466,45.528 L23.9710685,45.528 C23.3358904,45.528 22.8111781,44.996 22.8111781,44.352 C22.8111781,43.708 23.3358904,43.176 23.9710685,43.176 Z" id="Path" fill="#E1DBF1" fill-rule="nonzero"></path> - <path d="M40.32,36.092 L42.6673973,36.092 C43.3025753,36.092 43.8272877,36.624 43.8272877,37.268 C43.8272877,37.912 43.3025753,38.444 42.6673973,38.444 L40.32,38.444 C39.6848219,38.444 39.1601096,37.912 39.1601096,37.268 C39.1324932,36.624 39.6572055,36.092 40.32,36.092 Z" id="Path" fill="#FC6D26" fill-rule="nonzero"></path> - <path d="M52.2503014,33.712 C53.0511781,33.712 53.7139726,34.384 53.7139726,35.196 C53.7139726,36.008 53.0511781,36.68 52.2503014,36.68 C51.4494247,36.68 50.7866301,36.008 50.7866301,35.196 C50.8142466,34.384 51.4494247,33.712 52.2503014,33.712 Z M58.1049863,50.876 C58.905863,50.876 59.5686575,51.548 59.5686575,52.36 C59.5686575,53.172 58.905863,53.844 58.1049863,53.844 C57.3041096,53.844 56.6413151,53.172 56.6413151,52.36 C56.6413151,51.548 57.3041096,50.876 58.1049863,50.876 Z" id="Shape" fill="#E1DBF1" fill-rule="nonzero"></path> - <path d="M63.3521096,50.876 C64.1529863,50.876 64.8157808,51.548 64.8157808,52.36 C64.8157808,53.172 64.1529863,53.844 63.3521096,53.844 C62.5512329,53.844 61.8884384,53.172 61.8884384,52.36 C61.8884384,51.548 62.5512329,50.876 63.3521096,50.876 Z M33.3054247,14.784 L35.6528219,14.784 C36.288,14.784 36.8127123,15.316 36.8127123,15.96 C36.8127123,16.604 36.288,17.136 35.6528219,17.136 L33.3054247,17.136 C32.6702466,17.136 32.1455342,16.604 32.1455342,15.96 C32.1455342,15.316 32.6702466,14.784 33.3054247,14.784 Z" id="Shape" fill="#FC6D26" fill-rule="nonzero"></path> - <path d="M14.6367123,36.092 L28.6382466,36.092 C29.2734247,36.092 29.798137,36.624 29.798137,37.268 C29.798137,37.912 29.2734247,38.444 28.6382466,38.444 L14.6367123,38.444 C14.0015342,38.444 13.4768219,37.912 13.4768219,37.268 C13.4768219,36.624 14.0015342,36.092 14.6367123,36.092 Z M44.0482192,42 L61.0599452,42 C61.8332055,42 62.4683836,42.672 62.4683836,43.484 C62.4683836,44.296 61.8332055,44.968 61.0599452,44.968 L44.0758356,44.968 C43.3025753,44.968 42.6673973,44.296 42.6673973,43.484 C42.6673973,42.672 43.2749589,42 44.0482192,42 L44.0482192,42 L44.0482192,42 Z" id="Shape" fill="#EEEEEE" fill-rule="nonzero"></path> - <path d="M35.3214247,50.876 L52.3055342,50.876 C53.0787945,50.876 53.7139726,51.548 53.7139726,52.36 C53.7139726,53.172 53.0787945,53.844 52.3055342,53.844 L35.3214247,53.844 C34.5481644,53.844 33.9129863,53.172 33.9129863,52.36 C33.8853699,51.548 34.5205479,50.876 35.3214247,50.876 L35.3214247,50.876 L35.3214247,50.876 Z" id="Path" fill="#EFEDF8" fill-rule="nonzero"></path> - <path d="M14.6367123,21.896 L28.6382466,21.896 C29.2734247,21.896 29.798137,22.428 29.798137,23.072 C29.798137,23.716 29.2734247,24.248 28.6382466,24.248 L14.6367123,24.248 C14.0015342,24.248 13.4768219,23.716 13.4768219,23.072 C13.4768219,22.428 14.0015342,21.896 14.6367123,21.896 Z" id="Path" fill="#6B4FBB" fill-rule="nonzero"></path> - <path d="M33.3054247,28.98 L47.3069589,28.98 C47.942137,28.98 48.4668493,29.512 48.4668493,30.156 C48.4668493,30.8 47.942137,31.332 47.3069589,31.332 L33.3054247,31.332 C32.6702466,31.332 32.1455342,30.8 32.1455342,30.156 C32.1455342,29.512 32.6702466,28.98 33.3054247,28.98 Z" id="Path" fill="#C3B8E3" fill-rule="nonzero"></path> - <path d="M14.6367123,28.98 L16.9841096,28.98 C17.6192877,28.98 18.144,29.512 18.144,30.156 C18.144,30.8 17.6192877,31.332 16.9841096,31.332 L14.6367123,31.332 C14.0015342,31.332 13.4768219,30.8 13.4768219,30.156 C13.4768219,29.512 14.0015342,28.98 14.6367123,28.98 Z" id="Path" fill="#FEF0E8" fill-rule="nonzero"></path> - <path d="M33.3054247,43.176 L35.6528219,43.176 C36.288,43.176 36.8127123,43.708 36.8127123,44.352 C36.8127123,44.996 36.288,45.528 35.6528219,45.528 L33.3054247,45.528 C32.6702466,45.528 32.1455342,44.996 32.1455342,44.352 C32.1455342,43.708 32.6702466,43.176 33.3054247,43.176 Z" id="Path" fill="#6B4FBB" fill-rule="nonzero"></path> - <path d="M14.6367123,43.176 L19.3038904,43.176 C19.9390685,43.176 20.4637808,43.708 20.4637808,44.352 C20.4637808,44.996 19.9390685,45.528 19.3038904,45.528 L14.6367123,45.528 C14.0015342,45.528 13.4768219,44.996 13.4768219,44.352 C13.4768219,43.708 14.0015342,43.176 14.6367123,43.176 Z" id="Path" fill="#FC6D26" fill-rule="nonzero"></path> - <path d="M14.6367123,50.288 L19.3038904,50.288 C19.9390685,50.288 20.4637808,50.82 20.4637808,51.464 C20.4637808,52.108 19.9390685,52.64 19.3038904,52.64 L14.6367123,52.64 C14.0015342,52.64 13.4768219,52.108 13.4768219,51.464 C13.4768219,50.82 14.0015342,50.288 14.6367123,50.288 Z M23.9710685,50.288 L28.6382466,50.288 C29.2734247,50.288 29.798137,50.82 29.798137,51.464 C29.798137,52.108 29.2734247,52.64 28.6382466,52.64 L23.9710685,52.64 C23.3358904,52.64 22.8111781,52.108 22.8111781,51.464 C22.8111781,50.82 23.3358904,50.288 23.9710685,50.288 Z" id="Shape" fill="#FEF0E8" fill-rule="nonzero"></path> - <path d="M14.6367123,57.372 L21.6236712,57.372 C22.2588493,57.372 22.7835616,57.904 22.7835616,58.548 C22.7835616,59.192 22.2588493,59.724 21.6236712,59.724 L14.6367123,59.724 C14.0015342,59.724 13.4768219,59.192 13.4768219,58.548 C13.4768219,57.904 14.0015342,57.372 14.6367123,57.372 Z" id="Path" fill="#EFEDF8" fill-rule="nonzero"></path> - <path d="M25.3518904,64.484 L33.6644384,64.484 C34.4376986,64.484 35.0452603,65.016 35.0452603,65.66 C35.0452603,66.304 34.4376986,66.836 33.6644384,66.836 L25.3518904,66.836 C24.5786301,66.836 23.9710685,66.304 23.9710685,65.66 C23.9710685,65.016 24.5786301,64.484 25.3518904,64.484 Z" id="Path" fill="#FC6D26" fill-rule="nonzero"></path> - <path d="M44.0206027,59.136 L52.3331507,59.136 C53.106411,59.136 53.7139726,59.808 53.7139726,60.62 C53.7139726,61.432 53.106411,62.104 52.3331507,62.104 L44.0206027,62.104 C43.2473425,62.104 42.6397808,61.432 42.6397808,60.62 C42.6397808,59.808 43.2473425,59.136 44.0206027,59.136 Z" id="Path" fill="#6B4FBB" fill-rule="nonzero"></path> - <path d="M26.2908493,57.372 L28.6382466,57.372 C29.2734247,57.372 29.798137,57.904 29.798137,58.548 C29.798137,59.192 29.2734247,59.724 28.6382466,59.724 L26.2908493,59.724 C25.6556712,59.724 25.1309589,59.192 25.1309589,58.548 C25.1309589,57.904 25.6556712,57.372 26.2908493,57.372 Z" id="Path" fill="#FEE1D3" fill-rule="nonzero"></path> - <path d="M36.8127123,64.484 L39.1601096,64.484 C39.7952877,64.484 40.32,65.016 40.32,65.66 C40.32,66.304 39.7952877,66.836 39.1601096,66.836 L36.8127123,66.836 C36.1775342,66.836 35.6528219,66.304 35.6528219,65.66 C35.6528219,65.016 36.1775342,64.484 36.8127123,64.484 Z M58.1049863,59.136 L61.0323288,59.136 C61.8332055,59.136 62.496,59.808 62.496,60.62 C62.496,61.432 61.8332055,62.104 61.0323288,62.104 L58.1049863,62.104 C57.3041096,62.104 56.6413151,61.432 56.6413151,60.62 C56.6413151,59.808 57.3041096,59.136 58.1049863,59.136 Z" id="Shape" fill="#FEF0E8" fill-rule="nonzero"></path> - <path d="M35.3490411,59.136 L38.2763836,59.136 C39.0772603,59.136 39.7400548,59.808 39.7400548,60.62 C39.7400548,61.432 39.0772603,62.104 38.2763836,62.104 L35.3490411,62.104 C34.5481644,62.104 33.8853699,61.432 33.8853699,60.62 C33.8853699,59.808 34.5481644,59.136 35.3490411,59.136 Z" id="Path" fill="#FC6D26" fill-rule="nonzero"></path> - <path d="M14.6367123,64.484 L20.4637808,64.484 C21.0989589,64.484 21.6236712,65.016 21.6236712,65.66 C21.6236712,66.304 21.0989589,66.836 20.4637808,66.836 L14.6367123,66.836 C14.0015342,66.836 13.4768219,66.304 13.4768219,65.66 C13.4768219,65.016 14.0015342,64.484 14.6367123,64.484 Z" id="Path" fill="#EEEEEE" fill-rule="nonzero"></path> - <ellipse id="Oval" fill="#FFFFFF" fill-rule="nonzero" cx="36.7574795" cy="38.612" rx="20.4085479" ry="20.692"></ellipse> - <path d="M36.7574795,59.304 C25.4899726,59.304 16.3489315,50.036 16.3489315,38.612 C16.3489315,27.188 25.4899726,17.92 36.7574795,17.92 C48.0249863,17.92 57.1660274,27.188 57.1660274,38.612 C57.1660274,50.036 48.0526027,59.304 36.7574795,59.304 Z M36.7574795,56.952 C46.7546301,56.952 54.8462466,48.748 54.8462466,38.612 C54.8462466,28.476 46.7546301,20.272 36.7574795,20.272 C26.7603288,20.272 18.6687123,28.476 18.6687123,38.612 C18.6687123,48.748 26.7879452,56.952 36.7574795,56.952 Z" id="Shape" fill="#EEEEEE" fill-rule="nonzero"></path> - <g transform="translate(26.787945, 29.400000)" id="Path"> - <path d="M19.7457534,10.528 L18.6410959,7.112 L16.4593973,0.336 C16.3489315,7.97972799e-15 15.8794521,7.97972799e-15 15.7413699,0.336 L13.5872877,7.112 L6.37939726,7.112 L4.19769863,0.336 C4.08723288,7.97972799e-15 3.61775342,7.97972799e-15 3.47967123,0.336 L1.2979726,7.112 L0.193315068,10.528 C0.0828493151,10.836 0.193315068,11.172 0.469479452,11.368 L9.94191781,18.34 L19.4143562,11.368 C19.718137,11.172 19.8286027,10.836 19.7457534,10.528" fill="#FC6D26"></path> - <polygon fill="#E24329" points="9.96953425 18.34 13.5596712 7.112 6.35178082 7.112"></polygon> - <polygon fill="#FC6D26" points="9.96953425 18.34 6.37939726 7.112 1.32558904 7.112"></polygon> - <path d="M1.32558904,7.112 L0.220931507,10.528 C0.110465753,10.836 0.220931507,11.172 0.49709589,11.368 L9.96953425,18.34 L1.32558904,7.112 Z" fill="#FCA326"></path> - <path d="M1.32558904,7.112 L6.37939726,7.112 L4.19769863,0.336 C4.08723288,7.9658502e-15 3.61775342,7.9658502e-15 3.47967123,0.336 L1.32558904,7.112 Z" fill="#E24329"></path> - <polygon fill="#FC6D26" points="9.96953425 18.34 13.5596712 7.112 18.6134795 7.112"></polygon> - <path d="M18.6410959,7.112 L19.7457534,10.528 C19.8562192,10.836 19.7457534,11.172 19.469589,11.368 L9.99715068,18.34 L18.6410959,7.112 Z" fill="#FCA326"></path> - <path d="M18.6410959,7.112 L13.5872877,7.112 L15.7689863,0.336 C15.8794521,7.9658502e-15 16.3489315,7.9658502e-15 16.4870137,0.336 L18.6410959,7.112 Z" fill="#E24329"></path> - </g> - </g> - <path d="M76,41.475 C76,40.660967 76.8066938,40 77.8150611,40 L81.4537653,40 C82.4578417,40 83.2688264,40.6540094 83.2688264,41.475 C83.2688264,42.289033 82.4621326,42.95 81.4537653,42.95 L77.8150611,42.95 C76.8152757,42.95 76,42.2959906 76,41.4819575 C76,41.4784788 76,41.4784788 76,41.475 Z M88.7311736,41.475 C88.7311736,40.660967 89.5378674,40 90.5462347,40 L94.1849389,40 C95.1890152,40 96,40.6540094 96,41.475 C96,42.289033 95.1933062,42.95 94.1849389,42.95 L90.5462347,42.95 C89.5464493,42.95 88.7311736,42.2959906 88.7311736,41.4819575 C88.7311736,41.4784788 88.7311736,41.4784788 88.7311736,41.475 Z" id="Shape" fill="#E5E5E5" fill-rule="nonzero"></path> - <g id="Group"> - <g transform="translate(3.686038, 58.800000)"> - <path d="M6.33328302,0.207529412 L17.1233208,0.207529412 C20.541283,0.207529412 23.2890566,3.04376471 23.2890566,6.57176471 L23.2890566,17.7091765 C23.2890566,21.2025882 20.541283,24.0734118 17.1233208,24.0734118 L6.33328302,24.0734118 C2.94883019,24.0734118 0.16754717,21.2371765 0.16754717,17.7091765 L0.16754717,6.57176471 C0.16754717,3.04376471 2.91532075,0.207529412 6.33328302,0.207529412 Z" id="Path" fill="#FFFFFF"></path> - <path d="M6.33328302,3.38964706 C4.62430189,3.38964706 3.25041509,4.80776471 3.25041509,6.57176471 L3.25041509,17.7091765 C3.25041509,19.4731765 4.62430189,20.8912941 6.33328302,20.8912941 L17.1233208,20.8912941 C18.8323019,20.8912941 20.2061887,19.4731765 20.2061887,17.7091765 L20.2061887,6.57176471 C20.2061887,4.80776471 18.8323019,3.38964706 17.1233208,3.38964706 L6.33328302,3.38964706 Z M6.33328302,0.207529412 L17.1233208,0.207529412 C20.541283,0.207529412 23.2890566,3.04376471 23.2890566,6.57176471 L23.2890566,17.7091765 C23.2890566,21.2025882 20.541283,24.0734118 17.1233208,24.0734118 L6.33328302,24.0734118 C2.94883019,24.0734118 0.16754717,21.2371765 0.16754717,17.7091765 L0.16754717,6.57176471 C0.16754717,3.04376471 2.91532075,0.207529412 6.33328302,0.207529412 Z" id="Shape" fill="#E1DBF1" fill-rule="nonzero"></path> - <path d="M11.7283019,8.12823529 L11.7283019,8.12823529 C13.8393962,8.12823529 15.5818868,9.89223529 15.5818868,12.1058824 L15.5818868,12.1058824 C15.5818868,14.3195294 13.8729057,16.0835294 11.7283019,16.0835294 L11.7283019,16.0835294 C9.61720755,16.0835294 7.87471698,14.3195294 7.87471698,12.1058824 L7.87471698,12.1058824 C7.87471698,9.92682353 9.58369811,8.12823529 11.7283019,8.12823529 Z" id="Path" fill="#6B4FBB"></path> - </g> - <path d="M4.62430189,3.35505882 C3.78656604,3.35505882 3.08286792,4.08141176 3.08286792,4.94611765 L3.08286792,14.4924706 C3.08286792,15.3571765 3.78656604,16.0835294 4.62430189,16.0835294 L13.8729057,16.0835294 C14.7106415,16.0835294 15.4143396,15.3571765 15.4143396,14.4924706 L15.4143396,4.94611765 C15.4143396,4.08141176 14.7106415,3.35505882 13.8729057,3.35505882 L4.62430189,3.35505882 Z" id="Path" fill="#FFFFFF"></path> - <path d="M4.62430189,3.35505882 C3.78656604,3.35505882 3.08286792,4.08141176 3.08286792,4.94611765 L3.08286792,14.4924706 C3.08286792,15.3571765 3.78656604,16.0835294 4.62430189,16.0835294 L13.8729057,16.0835294 C14.7106415,16.0835294 15.4143396,15.3571765 15.4143396,14.4924706 L15.4143396,4.94611765 C15.4143396,4.08141176 14.7106415,3.35505882 13.8729057,3.35505882 L4.62430189,3.35505882 Z M4.62430189,0.172941176 L13.8729057,0.172941176 C16.4196226,0.172941176 18.4972075,2.31741176 18.4972075,4.94611765 L18.4972075,14.4924706 C18.4972075,17.1211765 16.4196226,19.2656471 13.8729057,19.2656471 L4.62430189,19.2656471 C2.07758491,19.2656471 -1.19049424e-15,17.1211765 -1.19049424e-15,14.4924706 L-1.19049424e-15,4.94611765 C-1.19049424e-15,2.31741176 2.07758491,0.172941176 4.62430189,0.172941176 Z" id="Shape" fill="#FDC4A8" fill-rule="nonzero"></path> - <path d="M9.24860377,6.53717647 L9.24860377,6.53717647 C10.9575849,6.53717647 12.3314717,7.95529412 12.3314717,9.71929412 L12.3314717,9.71929412 C12.3314717,11.4832941 10.9575849,12.9014118 9.24860377,12.9014118 L9.24860377,12.9014118 C7.53962264,12.9014118 6.16573585,11.4832941 6.16573585,9.71929412 L6.16573585,9.71929412 C6.16573585,7.95529412 7.53962264,6.53717647 9.24860377,6.53717647 Z" id="Path" fill="#FC6D26"></path> - <g transform="translate(35.184906, 23.174118)"> - <path d="M7.94173585,1.62564706 L27.9803774,1.62564706 C32.2360755,1.62564706 35.6875472,5.18823529 35.6875472,9.58094118 L35.6875472,30.2647059 C35.6875472,34.6574118 32.2360755,38.22 27.9803774,38.22 L7.94173585,38.22 C3.68603774,38.22 0.234566038,34.6574118 0.234566038,30.2647059 L0.234566038,9.58094118 C0.234566038,5.18823529 3.68603774,1.62564706 7.94173585,1.62564706 Z" id="Path" fill="#E1DBF1"></path> - <path d="M7.94173585,0.0345882353 L27.9803774,0.0345882353 C32.2360755,0.0345882353 35.6875472,3.59717647 35.6875472,7.98988235 L35.6875472,28.6736471 C35.6875472,33.0663529 32.2360755,36.6289412 27.9803774,36.6289412 L7.94173585,36.6289412 C3.68603774,36.6289412 0.234566038,33.0663529 0.234566038,28.6736471 L0.234566038,7.98988235 C0.234566038,3.59717647 3.68603774,0.0345882353 7.94173585,0.0345882353 Z" id="Path" fill="#E1DBF1"></path> - <path d="M7.94173585,3.21670588 C5.39501887,3.21670588 3.31743396,5.36117647 3.31743396,7.98988235 L3.31743396,28.6736471 C3.31743396,31.3023529 5.39501887,33.4468235 7.94173585,33.4468235 L27.9803774,33.4468235 C30.5270943,33.4468235 32.6046792,31.3023529 32.6046792,28.6736471 L32.6046792,7.98988235 C32.6046792,5.36117647 30.5270943,3.21670588 27.9803774,3.21670588 C27.9468679,3.21670588 7.94173585,3.21670588 7.94173585,3.21670588 Z M7.94173585,0.0345882353 L27.9803774,0.0345882353 C32.2360755,0.0345882353 35.6875472,3.59717647 35.6875472,7.98988235 L35.6875472,28.6736471 C35.6875472,33.0663529 32.2360755,36.6289412 27.9803774,36.6289412 L7.94173585,36.6289412 C3.68603774,36.6289412 0.234566038,33.0663529 0.234566038,28.6736471 L0.234566038,7.98988235 C0.234566038,3.59717647 3.68603774,0.0345882353 7.94173585,0.0345882353 Z" id="Shape" fill="#C3B8E3" fill-rule="nonzero"></path> - <path d="M14.1074717,12.7630588 L21.8146415,12.7630588 C22.6523774,12.7630588 23.3560755,13.4894118 23.3560755,14.3541176 L23.3560755,22.3094118 C23.3560755,23.1741176 22.6523774,23.9004706 21.8146415,23.9004706 L14.1074717,23.9004706 C13.2697358,23.9004706 12.5660377,23.1741176 12.5660377,22.3094118 L12.5660377,14.3541176 C12.5660377,13.4894118 13.2362264,12.7630588 14.1074717,12.7630588 Z" id="Path" fill="#6B4FBB"></path> - </g> - <path d="M32.6716981,71.4592941 C32.0685283,72.0818824 31.0967547,72.0818824 30.4935849,71.4592941 C29.8904151,70.8367059 29.8904151,69.8336471 30.4935849,69.2110588 L32.1355472,67.5162353 C32.738717,66.8936471 33.7104906,66.8936471 34.3136604,67.5162353 C34.9168302,68.1388235 34.9168302,69.1418824 34.3136604,69.7644706 L32.6716981,71.4592941 Z" id="Path" fill="#C3B8E3" fill-rule="nonzero"></path> - <path d="M37.5640755,66.4094118 C36.9609057,67.032 35.9891321,67.032 35.3859623,66.4094118 C34.7827925,65.7868235 34.7827925,64.7837647 35.3859623,64.1611765 L37.0279245,62.4663529 C37.6310943,61.8437647 38.6028679,61.8437647 39.2060377,62.4663529 C39.8092075,63.0889412 39.8092075,64.092 39.2060377,64.7145882 L37.5640755,66.4094118 Z" id="Path" fill="#E1DBF1"></path> - <path d="M21.3455094,21.2717647 C20.7088302,20.7183529 20.6083019,19.7152941 21.1444528,19.0235294 C21.6806038,18.3663529 22.6523774,18.2625882 23.322566,18.816 L25.098566,20.3378824 C25.7352453,20.8912941 25.8357736,21.8943529 25.2996226,22.5861176 C24.7634717,23.2432941 23.7916981,23.3470588 23.1215094,22.7936471 L21.3455094,21.2717647 Z" id="Path" fill="#E1DBF1" fill-rule="nonzero"></path> - <path d="M26.64,25.872 C26.0033208,25.3185882 25.9027925,24.3155294 26.4389434,23.6237647 C26.9750943,22.9665882 27.9468679,22.8628235 28.6170566,23.4162353 L30.3930566,24.9381176 C31.0297358,25.4915294 31.1302642,26.4945882 30.5941132,27.1863529 C30.0579623,27.8781176 29.0861887,27.9472941 28.416,27.3938824 L26.64,25.872 Z" id="Path" fill="#C3B8E3"></path> - </g> - </g> - </g> -</svg>
\ No newline at end of file +<svg width="169" height="78" viewBox="0 0 169 78" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M115.571 41.5714L147.714 41.5714C158.365 41.5714 167 32.9369 167 22.2857C167 11.6345 158.365 3 147.714 3C137.063 3 128.429 11.6345 128.429 22.2857C128.429 27.3128 130.352 31.8907 133.503 35.3235" stroke="#DBDBDB" stroke-width="2.63203" stroke-linecap="round"/> +<path d="M115.107 41.5714H125.786C133.084 41.5714 139 47.4877 139 54.7857C139 62.0838 133.084 68 125.786 68C118.488 68 112.571 62.0838 112.571 54.7857C112.571 53.039 112.91 51.3715 113.526 49.8453" stroke="#DBDBDB" stroke-width="2.63203" stroke-linecap="round"/> +<path d="M87.5486 37H76.3943C75.6243 37 75 36.3746 75 35.6032C75 34.8318 75.6243 34.2064 76.3943 34.2064H87.5486C88.3187 34.2064 88.9429 34.8318 88.9429 35.6032C88.9429 36.3746 88.3187 37 87.5486 37Z" fill="#FC6D26"/> +<path d="M118.703 37H96.3943C95.6243 37 95 36.3746 95 35.6032C95 34.8318 95.6243 34.2064 96.3943 34.2064H118.703C119.473 34.2064 120.097 34.8318 120.097 35.6032C120.097 36.3746 119.473 37 118.703 37Z" fill="#FC6D26"/> +<path d="M118.703 37H96.3943C95.6243 37 95 36.3746 95 35.6032C95 34.8318 95.6243 34.2064 96.3943 34.2064H118.703C119.473 34.2064 120.097 34.8318 120.097 35.6032C120.097 36.3746 119.473 37 118.703 37Z" fill="white" fill-opacity="0.6"/> +<path d="M93.8573 32H71.3944C70.6243 32 70.0001 31.3746 70.0001 30.6032C70.0001 29.8318 70.6243 29.2064 71.3944 29.2064L93.8573 29.2064C94.6273 29.2064 95.2516 29.8318 95.2516 30.6032C95.2516 31.3746 94.6273 32 93.8573 32Z" fill="#6E49CB"/> +<path d="M93.8573 32H71.3944C70.6243 32 70.0001 31.3746 70.0001 30.6032C70.0001 29.8318 70.6243 29.2064 71.3944 29.2064L93.8573 29.2064C94.6273 29.2064 95.2516 29.8318 95.2516 30.6032C95.2516 31.3746 94.6273 32 93.8573 32Z" fill="white" fill-opacity="0.8"/> +<path d="M86.8573 49H71.3944C70.6243 49 70.0001 48.3746 70.0001 47.6032C70.0001 46.8317 70.6243 46.2064 71.3944 46.2064H86.8573C87.6273 46.2064 88.2516 46.8317 88.2516 47.6032C88.2516 48.3746 87.6273 49 86.8573 49Z" fill="#6E49CB"/> +<path d="M86.8573 49H71.3944C70.6243 49 70.0001 48.3746 70.0001 47.6032C70.0001 46.8317 70.6243 46.2064 71.3944 46.2064H86.8573C87.6273 46.2064 88.2516 46.8317 88.2516 47.6032C88.2516 48.3746 87.6273 49 86.8573 49Z" fill="white" fill-opacity="0.8"/> +<path d="M109.166 43L73.3944 43C72.6243 43 72.0001 42.3746 72.0001 41.6032C72.0001 40.8317 72.6243 40.2064 73.3944 40.2064L109.166 40.2064C109.936 40.2064 110.56 40.8317 110.56 41.6032C110.56 42.3746 109.936 43 109.166 43Z" fill="#6E49CB"/> +<path d="M109.166 43L73.3944 43C72.6243 43 72.0001 42.3746 72.0001 41.6032C72.0001 40.8317 72.6243 40.2064 73.3944 40.2064L109.166 40.2064C109.936 40.2064 110.56 40.8317 110.56 41.6032C110.56 42.3746 109.936 43 109.166 43Z" fill="white" fill-opacity="0.4"/> +<path d="M146.262 24.2349L143.048 21.0153C142.767 20.7338 142.282 20.7323 141.983 21.0313L140.394 22.6236C140.1 22.9181 140.088 23.4002 140.378 23.6903L145.344 28.6651C145.841 29.1637 146.666 29.1795 147.166 28.6793L147.866 27.9779L155.864 19.9653C156.171 19.658 156.167 19.1776 155.868 18.8786L154.279 17.2863C153.985 16.9918 153.495 16.9891 153.194 17.2903L146.262 24.2349Z" fill="#FC6D26"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M125.682 56.7113L123.087 59.3221C122.858 59.5529 122.547 59.6825 122.223 59.6824C121.898 59.6824 121.587 59.5526 121.358 59.3218C121.129 59.091 121 58.7779 121 58.4515C121 58.1251 121.129 57.8121 121.358 57.5813L123.087 55.8412L121.358 54.1011C121.129 53.8703 121 53.5573 121 53.2309C121 52.9045 121.129 52.5915 121.358 52.3606C121.587 52.1298 121.898 52.0001 122.223 52C122.547 51.9999 122.858 52.1296 123.087 52.3603L125.682 54.9711C125.911 55.2019 126.04 55.5149 126.04 55.8412C126.04 56.1675 125.911 56.4805 125.682 56.7113ZM131.796 56.7113L129.202 59.3221C129.088 59.4364 128.954 59.527 128.805 59.5888C128.657 59.6506 128.498 59.6824 128.337 59.6824C128.177 59.6824 128.018 59.6505 127.869 59.5886C127.721 59.5268 127.586 59.4361 127.472 59.3218C127.359 59.2075 127.269 59.0718 127.207 58.9225C127.146 58.7732 127.114 58.6131 127.114 58.4515C127.114 58.2899 127.146 58.1299 127.208 57.9806C127.269 57.8313 127.359 57.6956 127.473 57.5813L129.202 55.8412L127.473 54.1011C127.359 53.9868 127.269 53.8512 127.208 53.7018C127.146 53.5525 127.114 53.3925 127.114 53.2309C127.114 53.0693 127.146 52.9092 127.207 52.7599C127.269 52.6106 127.359 52.4749 127.472 52.3606C127.586 52.2463 127.721 52.1556 127.869 52.0938C128.018 52.0319 128.177 52 128.337 52C128.498 52 128.657 52.0318 128.805 52.0936C128.954 52.1554 129.088 52.246 129.202 52.3603L131.796 54.9711C132.026 55.2019 132.154 55.5149 132.154 55.8412C132.154 56.1675 132.026 56.4805 131.796 56.7113Z" fill="#6E49CB"/> +<path d="M2 26C2 28.415 14.4361 30.3727 29.7769 30.3727C33.7709 30.3727 37.568 30.24 41 30.0011" stroke="#DBDBDB" stroke-width="1.28173"/> +<path d="M2 50C2 52.415 14.4361 54.3727 29.7769 54.3727C35.6133 54.3727 41.0293 54.0893 45.5 53.6052" stroke="#DBDBDB" stroke-width="1.28173"/> +<path d="M57.5537 5V22M2 5V68.6673C2 73.1731 20.9696 75.5204 29.7769 75.5204C38.5842 75.5204 57.5537 73.1731 57.5537 68.6673V57" stroke="#DBDBDB" stroke-width="2.56346" stroke-linejoin="round"/> +<ellipse cx="29.7769" cy="5.64391" rx="27.7769" ry="3.64391" stroke="#DBDBDB" stroke-width="2.56346"/> +<ellipse cx="55.4286" cy="39.46" rx="17.4286" ry="17.46" stroke="#6E49CB" stroke-width="2.56346"/> +<ellipse cx="55.2458" cy="39.2696" rx="13.2458" ry="13.2696" fill="#6E49CB"/> +<ellipse cx="55.2458" cy="39.2696" rx="13.2458" ry="13.2696" fill="white" fill-opacity="0.9"/> +<path d="M61.763 38.5893C62.5797 39.0892 62.5797 40.2756 61.763 40.7756L52.951 46.1704C52.0969 46.6933 51 46.0787 51 45.0773L51 34.2875C51 33.2861 52.0969 32.6715 52.951 33.1944L61.763 38.5893Z" fill="#6E49CB"/> +</svg> diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/create-from-template.svg b/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/create-from-template.svg index e90c354fe65..2ff4e4969b1 100644 --- a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/create-from-template.svg +++ b/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/create-from-template.svg @@ -1,54 +1,13 @@ -<?xml version="1.0" encoding="UTF-8"?> -<svg width="215px" height="115px" viewBox="0 0 215 115" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <!-- Generator: Sketch 61.2 (89653) - https://sketch.com --> - <title>create-project-from-template-md</title> - <desc>Created with Sketch.</desc> - <g id="create-project-from-template-md" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> - <g id="New-Template1" transform="translate(71.000000, 15.000000)"> - <g id="Group"> - <path d="M5.88230137,4.144 L61.8884384,4.144 C65.1195616,4.144 67.7155068,6.804 67.7155068,10.052 L67.7155068,78.064 C67.7155068,81.34 65.0919452,83.972 61.8884384,83.972 L5.88230137,83.972 C2.65117808,83.972 0.0552328767,81.312 0.0552328767,78.064 L0.0552328767,10.052 C0.0552328767,6.804 2.67879452,4.144 5.88230137,4.144 Z" id="Path" fill="#F9F9F9" fill-rule="nonzero"></path> - <path d="M8.82706849,1 L64.8332055,1 C68.0643288,1 70.660274,3.66 70.660274,6.908 L70.660274,74.332 C70.660274,77.608 68.0367123,80.24 64.8332055,80.24 L8.82706849,80.24 C5.59594521,80.24 3,77.58 3,74.332 L3,6.936 C3,3.66 5.59594521,1 8.82706849,1 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path> - <path d="M8.80964384,2.38 C6.24131507,2.38 4.14246575,4.508 4.14246575,7.112 L4.14246575,74.536 C4.14246575,77.14 6.24131507,79.268 8.80964384,79.268 L64.8157808,79.268 C67.3841096,79.268 69.4829589,77.14 69.4829589,74.536 L69.4829589,7.112 C69.4829589,4.508 67.3841096,2.38 64.8157808,2.38 L8.80964384,2.38 L8.80964384,2.38 Z M8.80964384,0 L64.8157808,0 C68.6820822,0 71.8027397,3.164 71.8027397,7.084 L71.8027397,74.508 C71.8027397,78.428 68.6820822,81.592 64.8157808,81.592 L8.80964384,81.592 C4.94334247,81.592 1.82250462,78.4 1.82250462,74.508 L1.82250462,7.112 C1.79506849,3.192 4.94334247,0 8.80964384,0 Z" id="Shape" fill="#EEEEEE" fill-rule="nonzero"></path> - <path d="M14.6367123,14.784 L21.6236712,14.784 C22.2588493,14.784 22.7835616,15.316 22.7835616,15.96 C22.7835616,16.604 22.2588493,17.136 21.6236712,17.136 L14.6367123,17.136 C14.0015342,17.136 13.4768219,16.604 13.4768219,15.96 C13.4768219,15.316 14.0015342,14.784 14.6367123,14.784 Z M33.3054247,21.896 L40.2923836,21.896 C40.9275616,21.896 41.452274,22.428 41.452274,23.072 C41.452274,23.716 40.9275616,24.248 40.2923836,24.248 L33.3054247,24.248 C32.6702466,24.248 32.1455342,23.716 32.1455342,23.072 C32.1455342,22.428 32.6702466,21.896 33.3054247,21.896 Z" id="Shape" fill="#E1DBF1" fill-rule="nonzero"></path> - <path d="M40.32,14.784 L47.3069589,14.784 C47.942137,14.784 48.4668493,15.316 48.4668493,15.96 C48.4668493,16.604 47.942137,17.136 47.3069589,17.136 L40.32,17.136 C39.6848219,17.136 39.1601096,16.604 39.1601096,15.96 C39.1324932,15.316 39.6572055,14.784 40.32,14.784 Z" id="Path" fill="#EEEEEE" fill-rule="nonzero"></path> - <path d="M21.6512877,28.98 L28.6382466,28.98 C29.2734247,28.98 29.798137,29.512 29.798137,30.156 C29.798137,30.8 29.2734247,31.332 28.6382466,31.332 L21.6512877,31.332 C21.0161096,31.332 20.4913973,30.8 20.4913973,30.156 C20.4637808,29.512 20.9884932,28.98 21.6512877,28.98 Z" id="Path" fill="#E1DBF1" fill-rule="nonzero"></path> - <path d="M26.2908493,14.784 L28.6382466,14.784 C29.2734247,14.784 29.798137,15.316 29.798137,15.96 C29.798137,16.604 29.2734247,17.136 28.6382466,17.136 L26.2908493,17.136 C25.6556712,17.136 25.1309589,16.604 25.1309589,15.96 C25.1309589,15.316 25.6556712,14.784 26.2908493,14.784 Z" id="Path" fill="#FEE1D3" fill-rule="nonzero"></path> - <path d="M33.3054247,36.092 L35.6528219,36.092 C36.288,36.092 36.8127123,36.624 36.8127123,37.268 C36.8127123,37.912 36.288,38.444 35.6528219,38.444 L33.3054247,38.444 C32.6702466,38.444 32.1455342,37.912 32.1455342,37.268 C32.1455342,36.624 32.6702466,36.092 33.3054247,36.092 Z" id="Path" fill="#FC6D26" fill-rule="nonzero"></path> - <path d="M44.9595616,21.896 L47.3069589,21.896 C47.942137,21.896 48.4668493,22.428 48.4668493,23.072 C48.4668493,23.716 47.942137,24.248 47.3069589,24.248 L44.9595616,24.248 C44.3243836,24.248 43.7996712,23.716 43.7996712,23.072 C43.7996712,22.428 44.3243836,21.896 44.9595616,21.896 Z M51.974137,14.784 L54.3215342,14.784 C54.9567123,14.784 55.4814247,15.316 55.4814247,15.96 C55.4814247,16.604 54.9567123,17.136 54.3215342,17.136 L51.974137,17.136 C51.3389589,17.136 50.8142466,16.604 50.8142466,15.96 C50.8142466,15.316 51.3389589,14.784 51.974137,14.784 Z" id="Shape" fill="#FEF0E8" fill-rule="nonzero"></path> - <path d="M23.9710685,43.176 L28.6382466,43.176 C29.2734247,43.176 29.798137,43.708 29.798137,44.352 C29.798137,44.996 29.2734247,45.528 28.6382466,45.528 L23.9710685,45.528 C23.3358904,45.528 22.8111781,44.996 22.8111781,44.352 C22.8111781,43.708 23.3358904,43.176 23.9710685,43.176 Z" id="Path" fill="#E1DBF1" fill-rule="nonzero"></path> - <path d="M40.32,36.092 L42.6673973,36.092 C43.3025753,36.092 43.8272877,36.624 43.8272877,37.268 C43.8272877,37.912 43.3025753,38.444 42.6673973,38.444 L40.32,38.444 C39.6848219,38.444 39.1601096,37.912 39.1601096,37.268 C39.1324932,36.624 39.6572055,36.092 40.32,36.092 Z" id="Path" fill="#FC6D26" fill-rule="nonzero"></path> - <path d="M52.2503014,33.712 C53.0511781,33.712 53.7139726,34.384 53.7139726,35.196 C53.7139726,36.008 53.0511781,36.68 52.2503014,36.68 C51.4494247,36.68 50.7866301,36.008 50.7866301,35.196 C50.8142466,34.384 51.4494247,33.712 52.2503014,33.712 Z M58.1049863,50.876 C58.905863,50.876 59.5686575,51.548 59.5686575,52.36 C59.5686575,53.172 58.905863,53.844 58.1049863,53.844 C57.3041096,53.844 56.6413151,53.172 56.6413151,52.36 C56.6413151,51.548 57.3041096,50.876 58.1049863,50.876 Z" id="Shape" fill="#E1DBF1" fill-rule="nonzero"></path> - <path d="M63.3521096,50.876 C64.1529863,50.876 64.8157808,51.548 64.8157808,52.36 C64.8157808,53.172 64.1529863,53.844 63.3521096,53.844 C62.5512329,53.844 61.8884384,53.172 61.8884384,52.36 C61.8884384,51.548 62.5512329,50.876 63.3521096,50.876 Z M33.3054247,14.784 L35.6528219,14.784 C36.288,14.784 36.8127123,15.316 36.8127123,15.96 C36.8127123,16.604 36.288,17.136 35.6528219,17.136 L33.3054247,17.136 C32.6702466,17.136 32.1455342,16.604 32.1455342,15.96 C32.1455342,15.316 32.6702466,14.784 33.3054247,14.784 Z" id="Shape" fill="#FC6D26" fill-rule="nonzero"></path> - <path d="M14.6367123,36.092 L28.6382466,36.092 C29.2734247,36.092 29.798137,36.624 29.798137,37.268 C29.798137,37.912 29.2734247,38.444 28.6382466,38.444 L14.6367123,38.444 C14.0015342,38.444 13.4768219,37.912 13.4768219,37.268 C13.4768219,36.624 14.0015342,36.092 14.6367123,36.092 Z M44.0482192,42 L61.0599452,42 C61.8332055,42 62.4683836,42.672 62.4683836,43.484 C62.4683836,44.296 61.8332055,44.968 61.0599452,44.968 L44.0758356,44.968 C43.3025753,44.968 42.6673973,44.296 42.6673973,43.484 C42.6673973,42.672 43.2749589,42 44.0482192,42 L44.0482192,42 L44.0482192,42 Z" id="Shape" fill="#EEEEEE" fill-rule="nonzero"></path> - <path d="M35.3214247,50.876 L52.3055342,50.876 C53.0787945,50.876 53.7139726,51.548 53.7139726,52.36 C53.7139726,53.172 53.0787945,53.844 52.3055342,53.844 L35.3214247,53.844 C34.5481644,53.844 33.9129863,53.172 33.9129863,52.36 C33.8853699,51.548 34.5205479,50.876 35.3214247,50.876 L35.3214247,50.876 L35.3214247,50.876 Z" id="Path" fill="#EFEDF8" fill-rule="nonzero"></path> - <path d="M14.6367123,21.896 L28.6382466,21.896 C29.2734247,21.896 29.798137,22.428 29.798137,23.072 C29.798137,23.716 29.2734247,24.248 28.6382466,24.248 L14.6367123,24.248 C14.0015342,24.248 13.4768219,23.716 13.4768219,23.072 C13.4768219,22.428 14.0015342,21.896 14.6367123,21.896 Z" id="Path" fill="#6B4FBB" fill-rule="nonzero"></path> - <path d="M33.3054247,28.98 L47.3069589,28.98 C47.942137,28.98 48.4668493,29.512 48.4668493,30.156 C48.4668493,30.8 47.942137,31.332 47.3069589,31.332 L33.3054247,31.332 C32.6702466,31.332 32.1455342,30.8 32.1455342,30.156 C32.1455342,29.512 32.6702466,28.98 33.3054247,28.98 Z" id="Path" fill="#C3B8E3" fill-rule="nonzero"></path> - <path d="M14.6367123,28.98 L16.9841096,28.98 C17.6192877,28.98 18.144,29.512 18.144,30.156 C18.144,30.8 17.6192877,31.332 16.9841096,31.332 L14.6367123,31.332 C14.0015342,31.332 13.4768219,30.8 13.4768219,30.156 C13.4768219,29.512 14.0015342,28.98 14.6367123,28.98 Z" id="Path" fill="#FEF0E8" fill-rule="nonzero"></path> - <path d="M33.3054247,43.176 L35.6528219,43.176 C36.288,43.176 36.8127123,43.708 36.8127123,44.352 C36.8127123,44.996 36.288,45.528 35.6528219,45.528 L33.3054247,45.528 C32.6702466,45.528 32.1455342,44.996 32.1455342,44.352 C32.1455342,43.708 32.6702466,43.176 33.3054247,43.176 Z" id="Path" fill="#6B4FBB" fill-rule="nonzero"></path> - <path d="M14.6367123,43.176 L19.3038904,43.176 C19.9390685,43.176 20.4637808,43.708 20.4637808,44.352 C20.4637808,44.996 19.9390685,45.528 19.3038904,45.528 L14.6367123,45.528 C14.0015342,45.528 13.4768219,44.996 13.4768219,44.352 C13.4768219,43.708 14.0015342,43.176 14.6367123,43.176 Z" id="Path" fill="#FC6D26" fill-rule="nonzero"></path> - <path d="M14.6367123,50.288 L19.3038904,50.288 C19.9390685,50.288 20.4637808,50.82 20.4637808,51.464 C20.4637808,52.108 19.9390685,52.64 19.3038904,52.64 L14.6367123,52.64 C14.0015342,52.64 13.4768219,52.108 13.4768219,51.464 C13.4768219,50.82 14.0015342,50.288 14.6367123,50.288 Z M23.9710685,50.288 L28.6382466,50.288 C29.2734247,50.288 29.798137,50.82 29.798137,51.464 C29.798137,52.108 29.2734247,52.64 28.6382466,52.64 L23.9710685,52.64 C23.3358904,52.64 22.8111781,52.108 22.8111781,51.464 C22.8111781,50.82 23.3358904,50.288 23.9710685,50.288 Z" id="Shape" fill="#FEF0E8" fill-rule="nonzero"></path> - <path d="M14.6367123,57.372 L21.6236712,57.372 C22.2588493,57.372 22.7835616,57.904 22.7835616,58.548 C22.7835616,59.192 22.2588493,59.724 21.6236712,59.724 L14.6367123,59.724 C14.0015342,59.724 13.4768219,59.192 13.4768219,58.548 C13.4768219,57.904 14.0015342,57.372 14.6367123,57.372 Z" id="Path" fill="#EFEDF8" fill-rule="nonzero"></path> - <path d="M25.3518904,64.484 L33.6644384,64.484 C34.4376986,64.484 35.0452603,65.016 35.0452603,65.66 C35.0452603,66.304 34.4376986,66.836 33.6644384,66.836 L25.3518904,66.836 C24.5786301,66.836 23.9710685,66.304 23.9710685,65.66 C23.9710685,65.016 24.5786301,64.484 25.3518904,64.484 Z" id="Path" fill="#FC6D26" fill-rule="nonzero"></path> - <path d="M44.0206027,59.136 L52.3331507,59.136 C53.106411,59.136 53.7139726,59.808 53.7139726,60.62 C53.7139726,61.432 53.106411,62.104 52.3331507,62.104 L44.0206027,62.104 C43.2473425,62.104 42.6397808,61.432 42.6397808,60.62 C42.6397808,59.808 43.2473425,59.136 44.0206027,59.136 Z" id="Path" fill="#6B4FBB" fill-rule="nonzero"></path> - <path d="M26.2908493,57.372 L28.6382466,57.372 C29.2734247,57.372 29.798137,57.904 29.798137,58.548 C29.798137,59.192 29.2734247,59.724 28.6382466,59.724 L26.2908493,59.724 C25.6556712,59.724 25.1309589,59.192 25.1309589,58.548 C25.1309589,57.904 25.6556712,57.372 26.2908493,57.372 Z" id="Path" fill="#FEE1D3" fill-rule="nonzero"></path> - <path d="M36.8127123,64.484 L39.1601096,64.484 C39.7952877,64.484 40.32,65.016 40.32,65.66 C40.32,66.304 39.7952877,66.836 39.1601096,66.836 L36.8127123,66.836 C36.1775342,66.836 35.6528219,66.304 35.6528219,65.66 C35.6528219,65.016 36.1775342,64.484 36.8127123,64.484 Z M58.1049863,59.136 L61.0323288,59.136 C61.8332055,59.136 62.496,59.808 62.496,60.62 C62.496,61.432 61.8332055,62.104 61.0323288,62.104 L58.1049863,62.104 C57.3041096,62.104 56.6413151,61.432 56.6413151,60.62 C56.6413151,59.808 57.3041096,59.136 58.1049863,59.136 Z" id="Shape" fill="#FEF0E8" fill-rule="nonzero"></path> - <path d="M35.3490411,59.136 L38.2763836,59.136 C39.0772603,59.136 39.7400548,59.808 39.7400548,60.62 C39.7400548,61.432 39.0772603,62.104 38.2763836,62.104 L35.3490411,62.104 C34.5481644,62.104 33.8853699,61.432 33.8853699,60.62 C33.8853699,59.808 34.5481644,59.136 35.3490411,59.136 Z" id="Path" fill="#FC6D26" fill-rule="nonzero"></path> - <path d="M14.6367123,64.484 L20.4637808,64.484 C21.0989589,64.484 21.6236712,65.016 21.6236712,65.66 C21.6236712,66.304 21.0989589,66.836 20.4637808,66.836 L14.6367123,66.836 C14.0015342,66.836 13.4768219,66.304 13.4768219,65.66 C13.4768219,65.016 14.0015342,64.484 14.6367123,64.484 Z" id="Path" fill="#EEEEEE" fill-rule="nonzero"></path> - <g id="Group-2" transform="translate(16.000000, 20.000000)"> - <ellipse id="Oval" fill="#FFFFFF" fill-rule="nonzero" cx="20.4085479" cy="20.692" rx="20.4085479" ry="20.692"></ellipse> - <path d="M20.4085479,41.384 C9.1410411,41.384 8.17124146e-14,32.116 8.17124146e-14,20.692 C8.17124146e-14,9.268 9.1410411,1.0658141e-14 20.4085479,1.0658141e-14 C31.6760548,1.0658141e-14 40.8170959,9.268 40.8170959,20.692 C40.8170959,32.116 31.7036712,41.384 20.4085479,41.384 Z M20.4085479,39.032 C30.4056986,39.032 38.4973151,30.828 38.4973151,20.692 C38.4973151,10.556 30.4056986,2.352 20.4085479,2.352 C10.4113973,2.352 2.31978082,10.556 2.31978082,20.692 C2.31978082,30.828 10.4390137,39.032 20.4085479,39.032 Z" id="Shape" fill="#EEEEEE" fill-rule="nonzero"></path> - <g id="Group" transform="translate(10.439014, 11.480000)"> - <path d="M19.7457534,10.528 L18.6410959,7.112 L16.4593973,0.336 C16.3489315,7.97972799e-15 15.8794521,7.97972799e-15 15.7413699,0.336 L13.5872877,7.112 L6.37939726,7.112 L4.19769863,0.336 C4.08723288,7.97972799e-15 3.61775342,7.97972799e-15 3.47967123,0.336 L1.2979726,7.112 L0.193315068,10.528 C0.0828493151,10.836 0.193315068,11.172 0.469479452,11.368 L9.94191781,18.34 L19.4143562,11.368 C19.718137,11.172 19.8286027,10.836 19.7457534,10.528" id="Path" fill="#FC6D26"></path> - <polygon id="Path" fill="#E24329" points="9.96953425 18.34 13.5596712 7.112 6.35178082 7.112"></polygon> - <polygon id="Path" fill="#FC6D26" points="9.96953425 18.34 6.37939726 7.112 1.32558904 7.112"></polygon> - <path d="M1.32558904,7.112 L0.220931507,10.528 C0.110465753,10.836 0.220931507,11.172 0.49709589,11.368 L9.96953425,18.34 L1.32558904,7.112 Z" id="Path" fill="#FCA326"></path> - <path d="M1.32558904,7.112 L6.37939726,7.112 L4.19769863,0.336 C4.08723288,7.9658502e-15 3.61775342,7.9658502e-15 3.47967123,0.336 L1.32558904,7.112 Z" id="Path" fill="#E24329"></path> - <polygon id="Path" fill="#FC6D26" points="9.96953425 18.34 13.5596712 7.112 18.6134795 7.112"></polygon> - <path d="M18.6410959,7.112 L19.7457534,10.528 C19.8562192,10.836 19.7457534,11.172 19.469589,11.368 L9.99715068,18.34 L18.6410959,7.112 Z" id="Path" fill="#FCA326"></path> - <path d="M18.6410959,7.112 L13.5872877,7.112 L15.7689863,0.336 C15.8794521,7.9658502e-15 16.3489315,7.9658502e-15 16.4870137,0.336 L18.6410959,7.112 Z" id="Path" fill="#E24329"></path> - </g> - </g> - </g> - </g> - </g> -</svg>
\ No newline at end of file +<svg width="82" height="80" viewBox="0 0 82 80" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M68.1765 8.17647H79.6471C80.2968 8.17647 80.8235 8.70319 80.8235 9.35294V69.6471C80.8235 70.2968 80.2968 70.8235 79.6471 70.8235H68.1765V8.17647Z" fill="#F0F0F0" stroke="#DBDBDB" stroke-width="2.35294"/> +<path d="M24.0504 19L24.0093 10.0746C24.0039 8.9145 24.8449 7.92363 25.9905 7.74035L65.393 1.43595C66.8226 1.20721 68.1176 2.31155 68.1176 3.75934V75.903C68.1176 77.3456 66.8314 78.4485 65.4057 78.2284L26.2788 72.1887C25.1356 72.0122 24.2902 71.0309 24.2849 69.8742L24.244 61" stroke="#DBDBDB" stroke-width="2.35294"/> +<path d="M60.0194 11.1796L30.0195 15.2198C29.4357 15.2984 29 15.7966 29 16.3857V19.1235C29 19.8153 29.594 20.3578 30.283 20.2951L60.283 17.5679C60.889 17.5128 61.3529 17.0047 61.3529 16.3962V12.3455C61.3529 11.6334 60.7252 11.0845 60.0194 11.1796Z" fill="#DBDBDB" stroke="#DBDBDB" stroke-width="0.588235" stroke-linecap="round" stroke-linejoin="bevel"/> +<path d="M51.1704 29.1021L41.8902 29.8481C41.0202 29.918 40.5266 30.8776 40.9756 31.626L42.6523 34.4205C42.8676 34.7793 43.2573 34.9968 43.6758 34.9916L51.2794 34.8968C51.9233 34.8888 52.4412 34.3645 52.4412 33.7205V30.2748C52.4412 29.5879 51.8551 29.0471 51.1704 29.1021Z" fill="#DBDBDB" stroke="#DBDBDB" stroke-width="0.588235" stroke-linecap="round" stroke-linejoin="bevel"/> +<path d="M61.2104 70.6341V40.1765C61.2104 39.5267 60.6837 39 60.0339 39H44.9909C44.4469 39 43.9738 39.373 43.8469 39.9019L41.118 51.2721C41.0819 51.4226 41.0148 51.5672 40.923 51.6918C37.1778 56.7763 34.7228 57.4741 29.7135 59.6826C29.2815 59.873 29.0064 60.3064 29.0162 60.7783L29.1309 66.295C29.1428 66.8693 29.5679 67.3511 30.1362 67.4345L59.8631 71.7981C60.5732 71.9024 61.2104 71.3519 61.2104 70.6341Z" fill="#DBDBDB" stroke="#DBDBDB" stroke-width="0.588235" stroke-linecap="round" stroke-linejoin="bevel"/> +<path d="M43.5694 24L36 24.5" stroke="#DBDBDB" stroke-width="1.17647" stroke-linecap="round"/> +<circle cx="23" cy="40" r="21" stroke="#6E49CB" stroke-width="2.35294"/> +<circle cx="23" cy="40" r="17" fill="#6E49CB"/> +<circle cx="23" cy="40" r="17" fill="white" fill-opacity="0.9"/> +<path d="M22.3125 48V33" stroke="#6E49CB" stroke-width="2.35294" stroke-linecap="round"/> +<path d="M15 41.3148H30" stroke="#6E49CB" stroke-width="2.35294" stroke-linecap="round"/> +</svg> diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/import-project.svg b/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/import-project.svg index c507fb8d73d..46b4b097bb6 100644 --- a/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/import-project.svg +++ b/app/assets/javascripts/projects/experiment_new_project_creation/illustrations/import-project.svg @@ -1,95 +1,38 @@ -<?xml version="1.0" encoding="UTF-8"?> -<svg width="215px" height="115px" viewBox="0 0 215 115" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <!-- Generator: Sketch 61.2 (89653) - https://sketch.com --> - <title>import-project-md</title> - <desc>Created with Sketch.</desc> - <g id="import-project-md" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> - <g id="Group-4" transform="translate(14.000000, 15.000000)"> - <g id="Group-7" transform="translate(0.000000, 0.007864)" fill-rule="nonzero"> - <path d="M5.84551724,4.12490066 L61.9641379,4.12490066 C65.1917241,4.12490066 67.8096552,6.76450331 67.8096552,10.0188079 L67.8096552,77.8001325 C67.8096552,81.0544371 65.1917241,83.6940397 61.9641379,83.6940397 L5.84551724,83.6940397 C2.61793103,83.6940397 2.84217094e-14,81.0544371 2.84217094e-14,77.8001325 L2.84217094e-14,10.0188079 C2.84217094e-14,6.76450331 2.61793103,4.12490066 5.84551724,4.12490066 Z" id="Path" fill="#F9F9F9"></path> - <path d="M8.76965517,1.17933775 L64.8882759,1.17933775 C68.1158621,1.17933775 70.7337931,3.8189404 70.7337931,7.07324503 L70.7337931,74.2649007 C70.7337931,77.5192053 68.1158621,80.1588079 64.8882759,80.1588079 L8.76965517,80.1588079 C5.54206897,80.1588079 2.92413793,77.5192053 2.92413793,74.2649007 L2.92413793,7.07324503 C2.92413793,3.81615894 5.53931034,1.17933775 8.76965517,1.17933775 Z" id="Path" fill="#FFFFFF"></path> - <path d="M64.8882759,2.20268248e-13 C68.7613793,2.20268248e-13 71.9034483,3.16807947 71.9034483,7.07324503 L71.9024828,19.847 L69.5634828,19.847 L69.5641379,7.07324503 C69.5641379,4.46980132 67.4703448,2.3586755 64.8882759,2.3586755 L8.76965517,2.3586755 L8.76965517,2.35589404 C6.18758621,2.35589404 4.0937931,4.46701987 4.0937931,7.07046358 L4.0937931,74.2621192 C4.0937931,76.8655629 6.18758621,78.9766887 8.76965517,78.9766887 L64.8882759,78.9766887 C67.4703448,78.9766887 69.5641379,76.8655629 69.5641379,74.2621192 L69.5624828,54.847 L71.9014828,54.847 L71.9034483,74.2649007 C71.9034483,78.1700662 68.7613793,81.3381457 64.8882759,81.3381457 L8.76965517,81.3381457 C4.89655172,81.3381457 1.75448276,78.1700662 1.75448276,74.2649007 L1.75448276,7.07324503 C1.75448276,3.16529801 4.8937931,2.20268248e-13 8.76965517,2.20268248e-13 L64.8882759,2.20268248e-13 Z M71.9014828,44.847 L71.9004828,48.847 L69.5614828,48.847 L69.5624828,44.847 L71.9014828,44.847 Z M71.9024828,26.847 L71.9014828,31.847 L69.5624828,31.847 L69.5634828,26.847 L71.9024828,26.847 Z" id="Combined-Shape" fill="#EEEEEE"></path> - <path d="M14.6151724,14.7333775 L21.6275862,14.7333775 C22.2731034,14.7333775 22.7972414,15.2618543 22.7972414,15.9127152 C22.7972414,16.5635762 22.2731034,17.092053 21.6275862,17.092053 L14.6151724,17.092053 C13.9696552,17.092053 13.4455172,16.5635762 13.4455172,15.9127152 C13.4455172,15.2618543 13.9696552,14.7333775 14.6151724,14.7333775 Z M33.3213793,21.8066225 L40.3365517,21.8066225 C40.982069,21.8066225 41.5062069,22.3350993 41.5062069,22.9859603 C41.5062069,23.6368212 40.982069,24.165298 40.3365517,24.165298 L33.3213793,24.165298 C32.6758621,24.165298 32.1517241,23.6368212 32.1517241,22.9859603 C32.1517241,22.3350993 32.6731034,21.8066225 33.3213793,21.8066225 Z" id="Shape" fill="#E1DBF1"></path> - <path d="M40.3337931,14.7333775 L47.3489655,14.7333775 C47.9944828,14.7333775 48.5186207,15.2618543 48.5186207,15.9127152 C48.5186207,16.5635762 47.9944828,17.092053 47.3489655,17.092053 L40.3337931,17.092053 C39.6882759,17.092053 39.1641379,16.5635762 39.1641379,15.9127152 C39.1668966,15.2618543 39.6882759,14.7333775 40.3337931,14.7333775 Z" id="Path" fill="#EEEEEE"></path> - <path d="M21.6275862,28.8798675 L28.6427586,28.8798675 C29.2882759,28.8798675 29.8124138,29.4083444 29.8124138,30.0592053 C29.8124138,30.7100662 29.2882759,31.238543 28.6427586,31.238543 L21.6275862,31.238543 C20.982069,31.238543 20.457931,30.7100662 20.457931,30.0592053 C20.4606897,29.4083444 20.982069,28.8798675 21.6275862,28.8798675 Z" id="Path" fill="#E1DBF1"></path> - <path d="M26.3062069,14.7333775 L28.6455172,14.7333775 C29.2910345,14.7333775 29.8151724,15.2618543 29.8151724,15.9127152 C29.8151724,16.5635762 29.2910345,17.092053 28.6455172,17.092053 L26.3062069,17.092053 C25.6606897,17.092053 25.1365517,16.5635762 25.1365517,15.9127152 C25.1365517,15.2618543 25.6606897,14.7333775 26.3062069,14.7333775 Z" id="Path" fill="#FEE1D3"></path> - <path d="M33.3213793,35.9531126 L35.6606897,35.9531126 C36.3062069,35.9531126 36.8303448,36.4815894 36.8303448,37.1324503 C36.8303448,37.7833113 36.3062069,38.3117881 35.6606897,38.3117881 L33.3213793,38.3117881 C32.6758621,38.3117881 32.1517241,37.7833113 32.1517241,37.1324503 C32.1517241,36.4815894 32.6731034,35.9531126 33.3213793,35.9531126 Z" id="Path" fill="#FC6D26"></path> - <path d="M52.0248276,14.7333775 L54.3641379,14.7333775 C55.0096552,14.7333775 55.5337931,15.2618543 55.5337931,15.9127152 C55.5337931,16.5635762 55.0096552,17.092053 54.3641379,17.092053 L52.0248276,17.092053 C51.3793103,17.092053 50.8551724,16.5635762 50.8551724,15.9127152 C50.857931,15.2618543 51.3793103,14.7333775 52.0248276,14.7333775 Z" id="Shape" fill="#FEF0E8"></path> - <path d="M23.9668966,43.0263576 L28.6427586,43.0263576 C29.2882759,43.0263576 29.8124138,43.5548344 29.8124138,44.2056954 C29.8124138,44.8565563 29.2882759,45.3850331 28.6427586,45.3850331 L23.9668966,45.3850331 C23.3213793,45.3850331 22.7972414,44.8565563 22.7972414,44.2056954 C22.7972414,43.5548344 23.3213793,43.0263576 23.9668966,43.0263576 Z" id="Path" fill="#E1DBF1"></path> - <path d="M40.3337931,35.9531126 L42.6731034,35.9531126 C43.3186207,35.9531126 43.8427586,36.4815894 43.8427586,37.1324503 C43.8427586,37.7833113 43.3186207,38.3117881 42.6731034,38.3117881 L40.3337931,38.3117881 C39.6882759,38.3117881 39.1641379,37.7833113 39.1641379,37.1324503 C39.1641379,36.4815894 39.6882759,35.9531126 40.3337931,35.9531126 Z" id="Path" fill="#FC6D26"></path> - <path d="M52.3172414,33.5944371 C53.1255172,33.5944371 53.7793103,34.2536424 53.7793103,35.0686093 C53.7793103,35.8835762 53.1255172,36.5427815 52.3172414,36.5427815 C51.5089655,36.5427815 50.8551724,35.8835762 50.8551724,35.0686093 C50.8551724,34.2536424 51.5117241,33.5944371 52.3172414,33.5944371 Z M58.1627586,50.6892715 C58.9710345,50.6892715 59.6248276,51.3484768 59.6248276,52.1634437 C59.6248276,52.9784106 58.9710345,53.6376159 58.1627586,53.6376159 C57.3544828,53.6376159 56.7006897,52.9784106 56.7006897,52.1634437 C56.7006897,51.3484768 57.3572414,50.6892715 58.1627586,50.6892715 Z" id="Shape" fill="#E1DBF1"></path> - <path d="M63.4262069,50.6892715 C64.2344828,50.6892715 64.8882759,51.3484768 64.8882759,52.1634437 C64.8882759,52.9784106 64.2344828,53.6376159 63.4262069,53.6376159 C62.617931,53.6376159 61.9641379,52.9784106 61.9641379,52.1634437 C61.9641379,51.3484768 62.617931,50.6892715 63.4262069,50.6892715 Z M33.3213793,14.7333775 L35.6606897,14.7333775 C36.3062069,14.7333775 36.8303448,15.2618543 36.8303448,15.9127152 C36.8303448,16.5635762 36.3062069,17.092053 35.6606897,17.092053 L33.3213793,17.092053 C32.6758621,17.092053 32.1517241,16.5635762 32.1517241,15.9127152 C32.1517241,15.2618543 32.6731034,14.7333775 33.3213793,14.7333775 Z" id="Shape" fill="#FC6D26"></path> - <path d="M59.1696552,33.8470199 L66.182069,33.8470199 C66.8275862,33.8470199 67.3517241,34.3754967 67.3517241,35.0263576 C67.3517241,35.6772185 66.8275862,36.2056954 66.182069,36.2056954 L59.1696552,36.2056954 C58.5241379,36.2056954 58,35.6772185 58,35.0263576 C58,34.3754967 58.5241379,33.8470199 59.1696552,33.8470199 Z" id="Shape" fill="#E1DBF1"></path> - <path d="M70.8606897,33.8470199 L73.2,33.8470199 C73.8455172,33.8470199 74.3696552,34.3754967 74.3696552,35.0263576 C74.3696552,35.6772185 73.8455172,36.2056954 73.2,36.2056954 L70.8606897,36.2056954 C70.2151724,36.2056954 69.6910345,35.6772185 69.6910345,35.0263576 C69.6910345,34.3754967 70.2151724,33.8470199 70.8606897,33.8470199 Z" id="Path" fill="#FEE1D3"></path> - <path d="M77.8758621,33.8470199 L80.2151724,33.8470199 C80.8606897,33.8470199 81.3848276,34.3754967 81.3848276,35.0263576 C81.3848276,35.6772185 80.8606897,36.2056954 80.2151724,36.2056954 L77.8758621,36.2056954 C77.2303448,36.2056954 76.7062069,35.6772185 76.7062069,35.0263576 C76.7062069,34.3754967 77.2275862,33.8470199 77.8758621,33.8470199 Z" id="Shape" fill="#FC6D26"></path> - <path d="M14.6151724,35.9531126 L28.6455172,35.9531126 C29.2910345,35.9531126 29.8151724,36.4815894 29.8151724,37.1324503 C29.8151724,37.7833113 29.2910345,38.3117881 28.6455172,38.3117881 L14.6151724,38.3117881 C13.9696552,38.3117881 13.4455172,37.7833113 13.4455172,37.1324503 C13.4455172,36.4815894 13.9696552,35.9531126 14.6151724,35.9531126 Z M44.0937931,41.8470199 L61.1282759,41.8470199 C61.9117241,41.8470199 62.5489655,42.5062252 62.5489655,43.3211921 C62.5489655,44.1361589 61.9144828,44.7953642 61.1282759,44.7953642 L44.0937931,44.7953642 C43.3103448,44.7953642 42.6731034,44.1361589 42.6731034,43.3211921 C42.6731034,42.5062252 43.3103448,41.8470199 44.0937931,41.8470199 L44.0937931,41.8470199 L44.0937931,41.8470199 Z" id="Shape" fill="#EEEEEE"></path> - <path d="M35.3241379,50.6892715 L52.3586207,50.6892715 C53.142069,50.6892715 53.7793103,51.3484768 53.7793103,52.1634437 C53.7793103,52.9784106 53.1448276,53.6376159 52.3586207,53.6376159 L35.3241379,53.6376159 C34.5406897,53.6376159 33.9034483,52.9784106 33.9034483,52.1634437 C33.9034483,51.3484768 34.5406897,50.6892715 35.3241379,50.6892715 L35.3241379,50.6892715 L35.3241379,50.6892715 Z" id="Path" fill="#EFEDF8"></path> - <path d="M14.6151724,21.8066225 L28.6455172,21.8066225 C29.2910345,21.8066225 29.8151724,22.3350993 29.8151724,22.9859603 C29.8151724,23.6368212 29.2910345,24.165298 28.6455172,24.165298 L14.6151724,24.165298 C13.9696552,24.165298 13.4455172,23.6368212 13.4455172,22.9859603 C13.4455172,22.3350993 13.9696552,21.8066225 14.6151724,21.8066225 Z" id="Path" fill="#6B4FBB"></path> - <path d="M33.3213793,28.8798675 L47.3517241,28.8798675 C47.9972414,28.8798675 48.5213793,29.4083444 48.5213793,30.0592053 C48.5213793,30.7100662 47.9972414,31.238543 47.3517241,31.238543 L33.3213793,31.238543 C32.6758621,31.238543 32.1517241,30.7100662 32.1517241,30.0592053 C32.1517241,29.4083444 32.6731034,28.8798675 33.3213793,28.8798675 Z" id="Path" fill="#C3B8E3"></path> - <path d="M14.6151724,28.8798675 L16.9544828,28.8798675 C17.6,28.8798675 18.1241379,29.4083444 18.1241379,30.0592053 C18.1241379,30.7100662 17.6,31.238543 16.9544828,31.238543 L14.6151724,31.238543 C13.9696552,31.238543 13.4455172,30.7100662 13.4455172,30.0592053 C13.4455172,29.4083444 13.9696552,28.8798675 14.6151724,28.8798675 Z" id="Path" fill="#FEF0E8"></path> - <path d="M75.182069,50.8470199 L82.1972414,50.8470199 C82.8427586,50.8470199 83.3668966,51.3754967 83.3668966,52.0263576 C83.3668966,52.6772185 82.8427586,53.2056954 82.1972414,53.2056954 L75.182069,53.2056954 C74.5365517,53.2056954 74.0124138,52.6772185 74.0124138,52.0263576 C74.0151724,51.3754967 74.5365517,50.8470199 75.182069,50.8470199 Z" id="Path" fill="#E1DBF1"></path> - <path d="M86.8758621,50.8470199 L100.906207,50.8470199 C101.551724,50.8470199 102.075862,51.5079868 102.075862,52.3220199 C102.075862,53.1360529 101.551724,53.7970199 100.906207,53.7970199 L86.8758621,53.7970199 C86.2303448,53.7970199 85.7062069,53.1360529 85.7062069,52.3220199 C85.7062069,51.5079868 86.2275862,50.8470199 86.8758621,50.8470199 Z" id="Path" fill="#C3B8E3"></path> - <path d="M68.1696552,50.8470199 L70.5089655,50.8470199 C71.1544828,50.8470199 71.6786207,51.3754967 71.6786207,52.0263576 C71.6786207,52.6772185 71.1544828,53.2056954 70.5089655,53.2056954 L68.1696552,53.2056954 C67.5241379,53.2056954 67,52.6772185 67,52.0263576 C67,51.3754967 67.5241379,50.8470199 68.1696552,50.8470199 Z" id="Path" fill="#FEF0E8"></path> - <path d="M33.3213793,43.0263576 L35.6606897,43.0263576 C36.3062069,43.0263576 36.8303448,43.5548344 36.8303448,44.2056954 C36.8303448,44.8565563 36.3062069,45.3850331 35.6606897,45.3850331 L33.3213793,45.3850331 C32.6758621,45.3850331 32.1517241,44.8565563 32.1517241,44.2056954 C32.1517241,43.5548344 32.6731034,43.0263576 33.3213793,43.0263576 Z" id="Path" fill="#6B4FBB"></path> - <path d="M14.6151724,43.0263576 L19.2910345,43.0263576 C19.9365517,43.0263576 20.4606897,43.5548344 20.4606897,44.2056954 C20.4606897,44.8565563 19.9365517,45.3850331 19.2910345,45.3850331 L14.6151724,45.3850331 C13.9696552,45.3850331 13.4455172,44.8565563 13.4455172,44.2056954 C13.4455172,43.5548344 13.9696552,43.0263576 14.6151724,43.0263576 Z" id="Path" fill="#FC6D26"></path> - <path d="M14.6151724,50.0996026 L19.2910345,50.0996026 C19.9365517,50.0996026 20.4606897,50.6280795 20.4606897,51.2789404 C20.4606897,51.9298013 19.9365517,52.4582781 19.2910345,52.4582781 L14.6151724,52.4582781 C13.9696552,52.4582781 13.4455172,51.9298013 13.4455172,51.2789404 C13.4455172,50.625298 13.9696552,50.0996026 14.6151724,50.0996026 Z M23.9668966,50.0996026 L28.6427586,50.0996026 C29.2882759,50.0996026 29.8124138,50.6280795 29.8124138,51.2789404 C29.8124138,51.9298013 29.2882759,52.4582781 28.6427586,52.4582781 L23.9668966,52.4582781 C23.3213793,52.4582781 22.7972414,51.9298013 22.7972414,51.2789404 C22.7972414,50.625298 23.3213793,50.0996026 23.9668966,50.0996026 Z" id="Shape" fill="#FEF0E8"></path> - <path d="M88.7172414,21.8029139 C89.5255172,21.8029139 90.1793103,22.4621192 90.1793103,23.2770861 C90.1793103,24.092053 89.5255172,24.7512583 88.7172414,24.7512583 C87.9089655,24.7512583 87.2551724,24.092053 87.2551724,23.2770861 C87.2551724,22.4621192 87.9117241,21.8029139 88.7172414,21.8029139 Z" id="Shape" fill="#FEE1D3"></path> - <path d="M93.9806897,21.8029139 C94.7889655,21.8029139 95.4427586,22.4621192 95.4427586,23.2770861 C95.4427586,24.092053 94.7889655,24.7512583 93.9806897,24.7512583 C93.1724138,24.7512583 92.5186207,24.092053 92.5186207,23.2770861 C92.5186207,22.4621192 93.1724138,21.8029139 93.9806897,21.8029139 Z" id="Shape" fill="#6B4FBB"></path> - <path d="M65.8786207,21.8029139 L82.9131034,21.8029139 C83.6965517,21.8029139 84.3337931,22.4624894 84.3337931,23.2779139 C84.3337931,24.0933384 83.6993103,24.7529139 82.9131034,24.7529139 L65.8786207,24.7529139 C65.0951724,24.7529139 64.457931,24.0933384 64.457931,23.2779139 C64.457931,22.4624894 65.0951724,21.8029139 65.8786207,21.8029139 L65.8786207,21.8029139 L65.8786207,21.8029139 Z" id="Path" fill="#FC6D26"></path> - <path d="M54.5213793,21.213245 L59.1972414,21.213245 C59.8427586,21.213245 60.3668966,21.7417219 60.3668966,22.3925828 C60.3668966,23.0434437 59.8427586,23.5719205 59.1972414,23.5719205 L54.5213793,23.5719205 C53.8758621,23.5719205 53.3517241,23.0434437 53.3517241,22.3925828 C53.3517241,21.7389404 53.8758621,21.213245 54.5213793,21.213245 Z" id="Shape" fill="#FEF0E8"></path> - <path d="M45.1696552,21.213245 L49.8455172,21.213245 C50.4910345,21.213245 51.0151724,21.7417219 51.0151724,22.3925828 C51.0151724,23.0434437 50.4910345,23.5719205 49.8455172,23.5719205 L45.1696552,23.5719205 C44.5241379,23.5719205 44,23.0434437 44,22.3925828 C44,21.7389404 44.5241379,21.213245 45.1696552,21.213245 Z" id="Path" fill="#EEEEEE"></path> - <path d="M14.6151724,57.1728477 L21.6275862,57.1728477 C22.2731034,57.1728477 22.7972414,57.7013245 22.7972414,58.3521854 C22.7972414,59.0030464 22.2731034,59.5315232 21.6275862,59.5315232 L14.6151724,59.5315232 C13.9696552,59.5315232 13.4455172,59.0030464 13.4455172,58.3521854 C13.4455172,57.698543 13.9696552,57.1728477 14.6151724,57.1728477 Z" id="Path" fill="#EFEDF8"></path> - <path d="M25.3544828,64.2433113 L33.6855172,64.2433113 C34.4524138,64.2433113 35.0731034,64.7717881 35.0731034,65.422649 C35.0731034,66.0735099 34.4524138,66.6019868 33.6855172,66.6019868 L25.3544828,66.6019868 C24.5875862,66.6019868 23.9668966,66.0735099 23.9668966,65.422649 C23.9668966,64.7717881 24.5875862,64.2433113 25.3544828,64.2433113 Z" id="Path" fill="#FC6D26"></path> - <path d="M44.0606897,58.9390728 L52.3917241,58.9390728 C53.1586207,58.9390728 53.7793103,59.5982781 53.7793103,60.413245 C53.7793103,61.2254305 53.1586207,61.8874172 52.3917241,61.8874172 L44.0606897,61.8874172 C43.2937931,61.8874172 42.6731034,61.2282119 42.6731034,60.413245 C42.6731034,59.6010596 43.2937931,58.9390728 44.0606897,58.9390728 Z" id="Path" fill="#6B4FBB"></path> - <path d="M26.3062069,57.1728477 L28.6455172,57.1728477 C29.2910345,57.1728477 29.8151724,57.7013245 29.8151724,58.3521854 C29.8151724,59.0030464 29.2910345,59.5315232 28.6455172,59.5315232 L26.3062069,59.5315232 C25.6606897,59.5315232 25.1365517,59.0030464 25.1365517,58.3521854 C25.1365517,57.698543 25.6606897,57.1728477 26.3062069,57.1728477 Z" id="Path" fill="#FEE1D3"></path> - <path d="M36.8275862,64.2433113 L39.1668966,64.2433113 C39.8124138,64.2433113 40.3365517,64.7717881 40.3365517,65.422649 C40.3365517,66.0735099 39.8124138,66.6019868 39.1668966,66.6019868 L36.8275862,66.6019868 C36.182069,66.6019868 35.657931,66.0735099 35.657931,65.422649 C35.657931,64.7717881 36.182069,64.2433113 36.8275862,64.2433113 Z M58.1627586,58.9390728 L61.0868966,58.9390728 C61.8951724,58.9390728 62.5489655,59.5982781 62.5489655,60.413245 C62.5489655,61.2282119 61.8951724,61.8874172 61.0868966,61.8874172 L58.1627586,61.8874172 C57.3544828,61.8874172 56.7006897,61.2282119 56.7006897,60.413245 C56.7034483,59.5982781 57.3572414,58.9390728 58.1627586,58.9390728 Z" id="Shape" fill="#FEF0E8"></path> - <path d="M35.3655172,58.9390728 L38.2896552,58.9390728 C39.097931,58.9390728 39.7517241,59.5982781 39.7517241,60.413245 C39.7517241,61.2282119 39.097931,61.8874172 38.2896552,61.8874172 L35.3655172,61.8874172 C34.5572414,61.8874172 33.9034483,61.2282119 33.9034483,60.413245 C33.9034483,59.5982781 34.56,58.9390728 35.3655172,58.9390728 Z" id="Path" fill="#FC6D26"></path> - <path d="M66.1696552,40.8470199 L73.182069,40.8470199 C73.8275862,40.8470199 74.3517241,41.3754967 74.3517241,42.0263576 C74.3517241,42.6772185 73.8275862,43.2056954 73.182069,43.2056954 L66.1696552,43.2056954 C65.5241379,43.2056954 65,42.6772185 65,42.0263576 C65,41.3727152 65.5241379,40.8470199 66.1696552,40.8470199 Z" id="Path" fill="#EFEDF8"></path> - <path d="M95.6151724,42.613245 L103.946207,42.613245 C104.713103,42.613245 105.333793,43.1409054 105.333793,43.793245 C105.333793,44.4433582 104.713103,44.973245 103.946207,44.973245 L95.6151724,44.973245 C94.8482759,44.973245 94.2275862,44.4455847 94.2275862,43.793245 C94.2275862,43.1431318 94.8482759,42.613245 95.6151724,42.613245 Z" id="Path" fill="#6B4FBB"></path> - <path d="M77.8606897,40.8470199 L80.2,40.8470199 C80.8455172,40.8470199 81.3696552,41.3754967 81.3696552,42.0263576 C81.3696552,42.6772185 80.8455172,43.2056954 80.2,43.2056954 L77.8606897,43.2056954 C77.2151724,43.2056954 76.6910345,42.6772185 76.6910345,42.0263576 C76.6910345,41.3727152 77.2151724,40.8470199 77.8606897,40.8470199 Z" id="Path" fill="#FEE1D3"></path> - <path d="M86.92,42.613245 L89.8441379,42.613245 C90.6524138,42.613245 91.3062069,43.1409054 91.3062069,43.793245 C91.3062069,44.4455847 90.6524138,44.973245 89.8441379,44.973245 L86.92,44.973245 C86.1117241,44.973245 85.457931,44.4455847 85.457931,43.793245 C85.457931,43.1409054 86.1144828,42.613245 86.92,42.613245 Z" id="Path" fill="#FC6D26"></path> - <path d="M14.6151724,64.2433113 L20.4606897,64.2433113 C21.1062069,64.2433113 21.6303448,64.7717881 21.6303448,65.422649 C21.6303448,66.0735099 21.1062069,66.6019868 20.4606897,66.6019868 L14.6151724,66.6019868 C13.9696552,66.6019868 13.4455172,66.0735099 13.4455172,65.422649 C13.4455172,64.7717881 13.9696552,64.2433113 14.6151724,64.2433113 Z" id="Path" fill="#EEEEEE"></path> - </g> - <g id="Group-12" transform="translate(112.058152, -0.000000)"> - <path d="M5.84861758,4.12465116 L62.0003099,4.12465116 C65.229233,4.12465116 67.8489253,6.76465116 67.8489253,10.0186047 L67.8489253,77.8046512 C67.8489253,81.0586047 65.229233,83.6986047 62.0003099,83.6986047 L5.84861758,83.6986047 C2.6196945,83.6986047 1.42108547e-14,81.0586047 1.42108547e-14,77.8046512 L1.42108547e-14,10.0213953 C-0.00276703963,6.76744186 2.6196945,4.12465116 5.84861758,4.12465116 Z" id="Path" fill="#F9F9F9" fill-rule="nonzero"></path> - <path d="M8.77292527,1.17767442 L64.9246176,1.17767442 C68.1535407,1.17767442 70.773233,3.81767442 70.773233,7.07162791 L70.773233,74.2688372 C70.773233,77.5227907 68.1535407,80.1627907 64.9246176,80.1627907 L8.77292527,80.1627907 C5.54400219,80.1627907 2.92430988,77.5227907 2.92430988,74.2688372 L2.92430988,7.07162791 C2.92430988,3.81767442 5.54400219,1.17767442 8.77292527,1.17767442 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path> - <path d="M8.77292527,2.35813953 C6.18646373,2.35813953 4.09292527,4.46790698 4.09292527,7.0744186 L4.09292527,74.2716279 C4.09292527,76.8781395 6.18646373,78.987907 8.77292527,78.987907 L64.9246176,78.987907 C67.5110791,78.987907 69.6046176,76.8781395 69.6046176,74.2716279 L69.6046176,7.07162791 C69.6046176,4.46511628 67.5110791,2.35534884 64.9246176,2.35534884 L8.77292527,2.35813953 L8.77292527,2.35813953 Z M8.77292527,-4.19220214e-13 L64.9246176,-4.19220214e-13 C68.8043099,-4.19220214e-13 71.9418483,3.16744186 71.9418483,7.07162791 L71.9418483,74.2688372 C71.9418483,78.1786047 68.7987714,81.3404651 64.9246176,81.3404651 L8.77292527,81.3404651 C4.89323296,81.3404651 1.75569267,78.1730233 1.75569267,74.2688372 L1.75569267,7.07162791 C1.75292527,3.17023256 4.89600219,-4.19220214e-13 8.77292527,-4.19220214e-13 Z" id="Shape" fill="#EEEEEE" fill-rule="nonzero"></path> - <path d="M14.6215407,14.7348837 L21.6387714,14.7348837 C22.281233,14.7348837 22.8073868,15.2651163 22.8073868,15.9125581 C22.8073868,16.56 22.281233,17.0902326 21.6387714,17.0902326 L14.6215407,17.0902326 C13.9790791,17.0902326 13.4529253,16.56 13.4529253,15.9125581 C13.4529253,15.2651163 13.9763099,14.7348837 14.6215407,14.7348837 Z M33.3387714,21.8093023 L40.3560022,21.8093023 C40.9984637,21.8093023 41.5246176,22.3395349 41.5246176,22.9869767 C41.5246176,23.6344186 40.9984637,24.1646512 40.3560022,24.1646512 L33.3387714,24.1646512 C32.6963099,24.1646512 32.170156,23.6344186 32.170156,22.9869767 C32.170156,22.3395349 32.6963099,21.8093023 33.3387714,21.8093023 Z" id="Shape" fill="#E1DBF1" fill-rule="nonzero"></path> - <path d="M40.3587714,14.7348837 L47.3760022,14.7348837 C48.0184637,14.7348837 48.5446176,15.2651163 48.5446176,15.9125581 C48.5446176,16.56 48.0184637,17.0902326 47.3760022,17.0902326 L40.3587714,17.0902326 C39.7163099,17.0902326 39.1901452,16.56 39.1901452,15.9125581 C39.1873868,15.267907 39.7163099,14.7348837 40.3587714,14.7348837 Z" id="Path" fill="#EEEEEE" fill-rule="nonzero"></path> - <path d="M21.6415407,28.8837209 L28.6587714,28.8837209 C29.301233,28.8837209 29.8273868,29.4139535 29.8273868,30.0613953 C29.8273868,30.7088372 29.301233,31.2390698 28.6587714,31.2390698 L21.6415407,31.2390698 C20.9990791,31.2390698 20.4729253,30.7088372 20.4729253,30.0613953 C20.4729253,29.4139535 20.9990791,28.8837209 21.6415407,28.8837209 Z" id="Path" fill="#E1DBF1" fill-rule="nonzero"></path> - <path d="M26.3187714,14.7348837 L28.6587714,14.7348837 C29.301233,14.7348837 29.8273868,15.2651163 29.8273868,15.9125581 C29.8273868,16.56 29.301233,17.0902326 28.6587714,17.0902326 L26.3187714,17.0902326 C25.6763099,17.0902326 25.150156,16.56 25.150156,15.9125581 C25.150156,15.2651163 25.6763099,14.7348837 26.3187714,14.7348837 Z" id="Path" fill="#FEE1D3" fill-rule="nonzero"></path> - <path d="M33.3387714,35.9553488 L35.6787714,35.9553488 C36.321233,35.9553488 36.8473868,36.4855814 36.8473868,37.1330233 C36.8473868,37.7804651 36.321233,38.3106977 35.6787714,38.3106977 L33.3387714,38.3106977 C32.6963099,38.3106977 32.170156,37.7804651 32.170156,37.1330233 C32.170156,36.4855814 32.6963099,35.9553488 33.3387714,35.9553488 Z" id="Path" fill="#FC6D26" fill-rule="nonzero"></path> - <path d="M45.0360022,21.8093023 L47.3760022,21.8093023 C48.0184637,21.8093023 48.5446176,22.3395349 48.5446176,22.9869767 C48.5446176,23.6344186 48.0184637,24.1646512 47.3760022,24.1646512 L45.0360022,24.1646512 C44.3935407,24.1646512 43.8673868,23.6344186 43.8673868,22.9869767 C43.8673868,22.3395349 44.3935407,21.8093023 45.0360022,21.8093023 Z M52.0560022,14.7348837 L54.3960022,14.7348837 C55.0384637,14.7348837 55.5646176,15.2651163 55.5646176,15.9125581 C55.5646176,16.56 55.0384637,17.0902326 54.3960022,17.0902326 L52.0560022,17.0902326 C51.4135407,17.0902326 50.8873868,16.56 50.8873868,15.9125581 C50.8873868,15.2651163 51.4135407,14.7348837 52.0560022,14.7348837 Z" id="Shape" fill="#FEF0E8" fill-rule="nonzero"></path> - <path d="M23.9787714,43.0297674 L28.6587714,43.0297674 C29.301233,43.0297674 29.8273868,43.56 29.8273868,44.2074419 C29.8273868,44.8548837 29.301233,45.3851163 28.6587714,45.3851163 L23.9787714,45.3851163 C23.3363099,45.3851163 22.810156,44.8548837 22.810156,44.2074419 C22.810156,43.56 23.3363099,43.0297674 23.9787714,43.0297674 Z" id="Path" fill="#E1DBF1" fill-rule="nonzero"></path> - <path d="M40.3587714,35.9553488 L42.6987714,35.9553488 C43.341233,35.9553488 43.8673868,36.4855814 43.8673868,37.1330233 C43.8673868,37.7804651 43.341233,38.3106977 42.6987714,38.3106977 L40.3587714,38.3106977 C39.7163099,38.3106977 39.1901452,37.7804651 39.1901452,37.1330233 C39.1873868,36.4883721 39.7163099,35.9553488 40.3587714,35.9553488 Z" id="Path" fill="#FC6D26" fill-rule="nonzero"></path> - <path d="M52.3495407,33.5972093 C53.158156,33.5972093 53.8116945,34.255814 53.8116945,35.0706977 C53.8116945,35.8855814 53.158156,36.544186 52.3495407,36.544186 C51.5409253,36.544186 50.8873868,35.8855814 50.8873868,35.0706977 C50.8873868,34.2586047 51.5436945,33.5972093 52.3495407,33.5972093 Z M58.198156,50.6930233 C59.0067714,50.6930233 59.6603099,51.3516279 59.6603099,52.1665116 C59.6603099,52.9813953 59.0067714,53.64 58.198156,53.64 C57.3895407,53.64 56.7360022,52.9813953 56.7360022,52.1665116 C56.7360022,51.3516279 57.3895407,50.6930233 58.198156,50.6930233 Z" id="Shape" fill="#E1DBF1" fill-rule="nonzero"></path> - <path d="M63.4624637,50.6930233 C64.2710791,50.6930233 64.9246176,51.3516279 64.9246176,52.1665116 C64.9246176,52.9813953 64.2710791,53.64 63.4624637,53.64 C62.6538483,53.64 62.0003099,52.9813953 62.0003099,52.1665116 C62.0003099,51.3516279 62.6566176,50.6930233 63.4624637,50.6930233 Z M33.3387714,14.7348837 L35.6787714,14.7348837 C36.321233,14.7348837 36.8473868,15.2651163 36.8473868,15.9125581 C36.8473868,16.56 36.321233,17.0902326 35.6787714,17.0902326 L33.3387714,17.0902326 C32.6963099,17.0902326 32.170156,16.56 32.170156,15.9125581 C32.170156,15.2651163 32.6963099,14.7348837 33.3387714,14.7348837 Z" id="Shape" fill="#FC6D26" fill-rule="nonzero"></path> - <path d="M14.6215407,35.9553488 L28.6615407,35.9553488 C29.3040022,35.9553488 29.830156,36.4855814 29.830156,37.1330233 C29.830156,37.7804651 29.3040022,38.3106977 28.6615407,38.3106977 L14.6215407,38.3106977 C13.9790791,38.3106977 13.4529253,37.7804651 13.4529253,37.1330233 C13.4529253,36.4855814 13.9763099,35.9553488 14.6215407,35.9553488 Z M44.1193868,41.8493023 L61.1640022,41.8493023 C61.9476945,41.8493023 62.5873868,42.507907 62.5873868,43.3227907 C62.5873868,44.1376744 61.9504637,44.7962791 61.1640022,44.7962791 L44.1193868,44.7962791 C43.3356945,44.7962791 42.6960022,44.1376744 42.6960022,43.3227907 C42.6960022,42.507907 43.3356945,41.8493023 44.1193868,41.8493023 L44.1193868,41.8493023 L44.1193868,41.8493023 Z" id="Shape" fill="#EEEEEE" fill-rule="nonzero"></path> - <path d="M35.3464637,50.6930233 L52.3910791,50.6930233 C53.1747714,50.6930233 53.8144637,51.3516279 53.8144637,52.1665116 C53.8144637,52.9813953 53.1775407,53.64 52.3910791,53.64 L35.3464637,53.64 C34.5627714,53.64 33.9230791,52.9813953 33.9230791,52.1665116 C33.9230791,51.3516279 34.5600022,50.6930233 35.3464637,50.6930233 L35.3464637,50.6930233 L35.3464637,50.6930233 Z" id="Path" fill="#EFEDF8" fill-rule="nonzero"></path> - <path d="M14.6215407,21.8093023 L28.6615407,21.8093023 C29.3040022,21.8093023 29.830156,22.3395349 29.830156,22.9869767 C29.830156,23.6344186 29.3040022,24.1646512 28.6615407,24.1646512 L14.6215407,24.1646512 C13.9790791,24.1646512 13.4529253,23.6344186 13.4529253,22.9869767 C13.4529253,22.3395349 13.9763099,21.8093023 14.6215407,21.8093023 Z" id="Path" fill="#6B4FBB" fill-rule="nonzero"></path> - <path d="M33.3387714,28.8837209 L47.3787714,28.8837209 C48.021233,28.8837209 48.5473868,29.4139535 48.5473868,30.0613953 C48.5473868,30.7088372 48.021233,31.2390698 47.3787714,31.2390698 L33.3387714,31.2390698 C32.6963099,31.2390698 32.170156,30.7088372 32.170156,30.0613953 C32.170156,29.4139535 32.6963099,28.8837209 33.3387714,28.8837209 Z" id="Path" fill="#C3B8E3" fill-rule="nonzero"></path> - <path d="M14.6215407,28.8837209 L16.9615407,28.8837209 C17.6040022,28.8837209 18.130156,29.4139535 18.130156,30.0613953 C18.130156,30.7088372 17.6040022,31.2390698 16.9615407,31.2390698 L14.6215407,31.2390698 C13.9790791,31.2390698 13.4529253,30.7088372 13.4529253,30.0613953 C13.4529253,29.4139535 13.9763099,28.8837209 14.6215407,28.8837209 Z" id="Path" fill="#FEF0E8" fill-rule="nonzero"></path> - <path d="M33.3387714,43.0297674 L35.6787714,43.0297674 C36.321233,43.0297674 36.8473868,43.56 36.8473868,44.2074419 C36.8473868,44.8548837 36.321233,45.3851163 35.6787714,45.3851163 L33.3387714,45.3851163 C32.6963099,45.3851163 32.170156,44.8548837 32.170156,44.2074419 C32.170156,43.56 32.6963099,43.0297674 33.3387714,43.0297674 Z" id="Path" fill="#6B4FBB" fill-rule="nonzero"></path> - <path d="M14.6215407,43.0297674 L19.3015407,43.0297674 C19.9440022,43.0297674 20.470156,43.56 20.470156,44.2074419 C20.470156,44.8548837 19.9440022,45.3851163 19.3015407,45.3851163 L14.6215407,45.3851163 C13.9790791,45.3851163 13.4529253,44.8548837 13.4529253,44.2074419 C13.4529253,43.56 13.9763099,43.0297674 14.6215407,43.0297674 Z" id="Path" fill="#FC6D26" fill-rule="nonzero"></path> - <path d="M14.6215407,50.104186 L19.3015407,50.104186 C19.9440022,50.104186 20.470156,50.6344186 20.470156,51.2818605 C20.470156,51.9293023 19.9440022,52.4595349 19.3015407,52.4595349 L14.6215407,52.4595349 C13.9790791,52.4595349 13.4529253,51.9293023 13.4529253,51.2818605 C13.4529253,50.6344186 13.9763099,50.104186 14.6215407,50.104186 Z M23.9787714,50.104186 L28.6587714,50.104186 C29.301233,50.104186 29.8273868,50.6344186 29.8273868,51.2818605 C29.8273868,51.9293023 29.301233,52.4595349 28.6587714,52.4595349 L23.9787714,52.4595349 C23.3363099,52.4595349 22.810156,51.9293023 22.810156,51.2818605 C22.810156,50.6344186 23.3363099,50.104186 23.9787714,50.104186 Z" id="Shape" fill="#FEF0E8" fill-rule="nonzero"></path> - <path d="M14.6215407,57.175814 L21.6387714,57.175814 C22.281233,57.175814 22.8073868,57.7060465 22.8073868,58.3534884 C22.8073868,59.0009302 22.281233,59.5311628 21.6387714,59.5311628 L14.6215407,59.5311628 C13.9790791,59.5311628 13.4529253,59.0009302 13.4529253,58.3534884 C13.4529253,57.7060465 13.9763099,57.175814 14.6215407,57.175814 Z" id="Path" fill="#EFEDF8" fill-rule="nonzero"></path> - <path d="M25.366156,64.2502326 L33.7015407,64.2502326 C34.4686176,64.2502326 35.0916945,64.7748837 35.0916945,65.427907 C35.0916945,66.0753488 34.4713868,66.6055814 33.7015407,66.6055814 L25.366156,66.6055814 C24.5990791,66.6055814 23.9760022,66.0753488 23.9760022,65.427907 C23.9787714,64.7804651 24.6018483,64.2502326 25.366156,64.2502326 Z" id="Path" fill="#FC6D26" fill-rule="nonzero"></path> - <path d="M44.0833868,58.9451163 L52.4187714,58.9451163 C53.1858483,58.9451163 53.8089253,59.6037209 53.8089253,60.4186047 C53.8089253,61.2334884 53.1886176,61.892093 52.4187714,61.892093 L44.0833868,61.892093 C43.3163099,61.892093 42.693233,61.2334884 42.693233,60.4186047 C42.6960022,59.6065116 43.3190791,58.9451163 44.0833868,58.9451163 Z" id="Path" fill="#6B4FBB" fill-rule="nonzero"></path> - <path d="M26.3187714,57.175814 L28.6587714,57.175814 C29.301233,57.175814 29.8273868,57.7060465 29.8273868,58.3534884 C29.8273868,59.0009302 29.301233,59.5311628 28.6587714,59.5311628 L26.3187714,59.5311628 C25.6763099,59.5311628 25.150156,59.0009302 25.150156,58.3534884 C25.150156,57.7060465 25.6763099,57.175814 26.3187714,57.175814 Z" id="Path" fill="#FEE1D3" fill-rule="nonzero"></path> - <path d="M36.850156,64.2502326 L39.190156,64.2502326 C39.8326176,64.2502326 40.3587714,64.7804651 40.3587714,65.427907 C40.3587714,66.0753488 39.8326176,66.6055814 39.190156,66.6055814 L36.850156,66.6055814 C36.2076945,66.6055814 35.6815407,66.0753488 35.6815407,65.427907 C35.6815407,64.7804651 36.2049253,64.2502326 36.850156,64.2502326 Z M58.198156,58.9451163 L61.1224637,58.9451163 C61.9310791,58.9451163 62.5846176,59.6037209 62.5846176,60.4186047 C62.5846176,61.2334884 61.9310791,61.892093 61.1224637,61.892093 L58.198156,61.892093 C57.3895407,61.892093 56.7360022,61.2334884 56.7360022,60.4186047 C56.7360022,59.6065116 57.3895407,58.9451163 58.198156,58.9451163 Z" id="Shape" fill="#FEF0E8" fill-rule="nonzero"></path> - <path d="M35.385233,58.9451163 L38.3095407,58.9451163 C39.118156,58.9451163 39.7716945,59.6037209 39.7716945,60.4186047 C39.7716945,61.2334884 39.118156,61.892093 38.3095407,61.892093 L35.385233,61.892093 C34.5766176,61.892093 33.9230791,61.2334884 33.9230791,60.4186047 C33.9230791,59.6065116 34.5793868,58.9451163 35.385233,58.9451163 Z" id="Path" fill="#FC6D26" fill-rule="nonzero"></path> - <path d="M14.6215407,64.2502326 L20.470156,64.2502326 C21.1126176,64.2502326 21.6387714,64.7804651 21.6387714,65.427907 C21.6387714,66.0753488 21.1126176,66.6055814 20.470156,66.6055814 L14.6215407,66.6055814 C13.9790791,66.6055814 13.4529253,66.0753488 13.4529253,65.427907 C13.4529253,64.7804651 13.9763099,64.2502326 14.6215407,64.2502326 Z" id="Path" fill="#EEEEEE" fill-rule="nonzero"></path> - <ellipse id="Oval" fill="#FFFFFF" fill-rule="nonzero" cx="36.814156" cy="38.4725581" rx="20.4729231" ry="20.6316279"></ellipse> - <path d="M36.814156,59.104186 C25.5073868,59.104186 16.341233,49.8669767 16.341233,38.4725581 C16.341233,27.0781395 25.5073868,17.8409302 36.814156,17.8409302 C48.1209253,17.8409302 57.2870791,27.0781395 57.2870791,38.4725581 C57.2870791,49.8669767 48.1209253,59.104186 36.814156,59.104186 Z M36.814156,56.7460465 C46.8276945,56.7460465 54.9470791,48.5637209 54.9470791,38.4725581 C54.9470791,28.3813953 46.8276945,20.1990698 36.814156,20.1990698 C26.8006176,20.1990698 18.681233,28.3813953 18.681233,38.4725581 C18.681233,48.5637209 26.8006176,56.7460465 36.814156,56.7460465 Z" id="Shape" fill="#EEEEEE" fill-rule="nonzero"></path> - <path d="M46.5895407,39.7813953 L45.490156,36.3739535 L43.3135407,29.6260465 C43.2027714,29.28 42.718156,29.28 42.5990791,29.6260465 L40.4280022,36.3739535 L33.2030791,36.3739535 L31.0264637,29.6260465 C30.9156945,29.28 30.4310791,29.28 30.3120022,29.6260465 L28.1353868,36.3739535 L27.0360022,39.7813953 C26.9363099,40.0939535 27.0470791,40.4288372 27.3046176,40.624186 L36.8086176,47.5786047 L46.3126176,40.624186 C46.581233,40.4288372 46.689233,40.0883721 46.5895407,39.7813953" id="Path" fill="#FC6D26"></path> - <polygon id="Path" fill="#E24329" points="36.814156 47.5813953 40.4280022 36.3767442 33.2030791 36.3767442"></polygon> - <polygon id="Path" fill="#FC6D26" points="36.814156 47.5813953 33.2003099 36.3767442 28.1353868 36.3767442"></polygon> - <path d="M28.138156,36.3739535 L27.0387714,39.7813953 C26.9390791,40.0939535 27.0498483,40.4288372 27.3073868,40.624186 L36.8113868,47.5786047 L28.138156,36.3739535 Z" id="Path" fill="#FCA326"></path> - <path d="M28.138156,36.3739535 L33.2030791,36.3739535 L31.0264637,29.6260465 C30.9156945,29.28 30.4310791,29.28 30.3120022,29.6260465 L28.138156,36.3739535 Z" id="Path" fill="#E24329"></path> - <polygon id="Path" fill="#FC6D26" points="36.814156 47.5813953 40.4280022 36.3767442 45.4929253 36.3767442"></polygon> - <path d="M45.4929253,36.3739535 L46.5923099,39.7813953 C46.6920022,40.0939535 46.581233,40.4288372 46.3236945,40.624186 L36.8196945,47.5786047 L45.4929253,36.3739535 Z" id="Path" fill="#FCA326"></path> - <path d="M45.4929253,36.3739535 L40.4280022,36.3739535 L42.6046176,29.6260465 C42.7153868,29.28 43.2000022,29.28 43.3190791,29.6260465 L45.4929253,36.3739535 Z" id="Path" fill="#E24329"></path> - </g> - </g> - </g> -</svg>
\ No newline at end of file +<svg width="169" height="84" viewBox="0 0 169 84" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0)"> +<path d="M153.5 74.5714H165.684C166.411 74.5714 167 73.9822 167 73.2554V8.74461C167 8.01779 166.411 7.42859 165.684 7.42859H153.5" stroke="#DBDBDB" stroke-width="2.63203"/> +<path d="M107.94 57L108.014 72.9062C108.017 73.5536 108.49 74.1026 109.13 74.2008L151.913 80.7674C152.71 80.8897 153.429 80.273 153.429 79.4666V2.54193C153.429 1.73264 152.705 1.11511 151.906 1.24226L108.829 8.09543C108.187 8.19744 107.716 8.7519 107.719 9.4012L107.771 20.5" stroke="#DBDBDB" stroke-width="2.63203"/> +<path d="M133.539 52.5313L122.91 51.9925M137.311 52.7225L148.969 53.3135" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/> +<path d="M132.224 43.9783L124 43.6955M135.998 44.1081L147.665 44.5092" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/> +<path d="M148.238 12.3644L131.189 14.604M117.282 16.4529L126.416 15.2311" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/> +<path d="M149.032 36.8519L131.839 37.0342M125 37.0852L127.024 37.0852" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/> +<path d="M112.038 66.3444L120.582 67.4102M148.266 70.8634L134.595 69.1581M125.025 67.9644L129.468 68.5186" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/> +<path d="M114.352 23.3947L116.215 23.2387M129.258 22.147L119.433 22.9693M137.388 21.4665L145.18 20.8143" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/> +<path d="M135.832 29.2067L125.981 29.5888M138.724 28.9864L146.537 28.6833" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/> +<path d="M115.114 59.5557L128.942 60.8796M133.782 61.3429L145.19 62.4351" stroke="#DFDFDF" stroke-width="1.31602" stroke-linecap="round"/> +<path d="M53.4286 42.4286H21.2857C10.6345 42.4286 2.00002 33.7941 2.00002 23.1429C2.00002 12.4917 10.6345 3.85718 21.2857 3.85718C31.9369 3.85718 40.5714 12.4917 40.5714 23.1429C40.5714 28.17 38.648 32.7479 35.4969 36.1807" stroke="#DBDBDB" stroke-width="2.63203" stroke-linecap="round"/> +<path d="M53.0361 42.4286H42.3571C35.0591 42.4286 29.1428 48.3448 29.1428 55.6429C29.1428 62.9409 35.0591 68.8572 42.3571 68.8572C49.6552 68.8572 55.5714 62.9409 55.5714 55.6429C55.5714 53.8962 55.2325 52.2287 54.6169 50.7025" stroke="#DBDBDB" stroke-width="2.63203" stroke-linecap="round"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4286 51.7144C38.4286 50.9254 39.0682 50.2858 39.8572 50.2858H44.1429C44.829 50.2858 45.4022 50.7695 45.5399 51.4146L47.7105 52.6677C48.3938 53.0622 48.6279 53.9359 48.2334 54.6192C47.3183 56.2042 45.5714 59.2248 45.4609 59.4191C45.1836 59.9063 44.7237 60.2858 44.1429 60.2858H39.8572C39.0682 60.2858 38.4286 59.6462 38.4286 58.8572V51.7144ZM39.8572 51.7144H44.1429V58.8572H39.8572L39.8572 51.7144ZM45.5714 56.3727L46.9962 53.9049L45.5714 53.0823V56.3727Z" fill="#FC6D26"/> +<path d="M25.5984 15.2331C25.8026 14.471 25.3503 13.6877 24.5882 13.4835C23.8261 13.2793 23.0428 13.7315 22.8386 14.4936L18.4017 31.0524C18.1975 31.8145 18.6497 32.5978 19.4118 32.802C20.1739 33.0062 20.9573 32.5539 21.1615 31.7918L25.5984 15.2331Z" fill="#6E49CB"/> +<path d="M17.2958 17.8469C17.8537 18.4048 17.8537 19.3093 17.2958 19.8672L14.0203 23.1428L17.2958 26.4183C17.8537 26.9762 17.8537 27.8807 17.2958 28.4386C16.738 28.9965 15.8334 28.9965 15.2755 28.4386L10.9898 24.1529C10.4319 23.595 10.4319 22.6905 10.9898 22.1326L15.2755 17.8469C15.8334 17.289 16.738 17.289 17.2958 17.8469Z" fill="#6E49CB"/> +<path d="M26.7041 17.8469C26.1462 18.4048 26.1462 19.3093 26.7041 19.8672L29.9797 23.1428L26.7041 26.4183C26.1462 26.9762 26.1462 27.8807 26.7041 28.4386C27.262 28.9965 28.1665 28.9965 28.7244 28.4386L33.0101 24.1529C33.568 23.595 33.568 22.6905 33.0101 22.1326L28.7244 17.8469C28.1665 17.289 27.262 17.289 26.7041 17.8469Z" fill="#6E49CB"/> +<path d="M50.5714 35.2857L62 35.2857C62.7889 35.2857 63.4285 35.9253 63.4285 36.7143C63.4285 37.5032 62.7889 38.1428 62 38.1428L50.5714 38.1428C49.7824 38.1428 49.1428 37.5032 49.1428 36.7143C49.1428 35.9253 49.7824 35.2857 50.5714 35.2857Z" fill="#FC6D26"/> +<path d="M50.5714 35.2857L62 35.2857C62.7889 35.2857 63.4285 35.9253 63.4285 36.7143C63.4285 37.5032 62.7889 38.1428 62 38.1428L50.5714 38.1428C49.7824 38.1428 49.1428 37.5032 49.1428 36.7143C49.1428 35.9253 49.7824 35.2857 50.5714 35.2857Z" fill="white" fill-opacity="0.6"/> +<path d="M70.5713 35.2857L83.4285 35.2857C84.2175 35.2857 84.8571 35.9253 84.8571 36.7143C84.8571 37.5032 84.2175 38.1428 83.4285 38.1428L70.5713 38.1428C69.7824 38.1428 69.1428 37.5032 69.1428 36.7143C69.1428 35.9253 69.7824 35.2857 70.5713 35.2857Z" fill="#FC6D26"/> +<path d="M76.2856 46.7144L92.1428 46.7144C92.9318 46.7144 93.5714 47.3539 93.5714 48.1429C93.5714 48.9319 92.9318 49.5715 92.1428 49.5715L76.2856 49.5715C75.4967 49.5715 74.8571 48.9319 74.8571 48.1429C74.8571 47.354 75.4967 46.7144 76.2856 46.7144Z" fill="#6E49CB"/> +<path d="M76.2856 46.7144L92.1428 46.7144C92.9318 46.7144 93.5714 47.3539 93.5714 48.1429C93.5714 48.9319 92.9318 49.5715 92.1428 49.5715L76.2856 49.5715C75.4967 49.5715 74.8571 48.9319 74.8571 48.1429C74.8571 47.354 75.4967 46.7144 76.2856 46.7144Z" fill="white" fill-opacity="0.8"/> +<path d="M62.7142 40.9999L90 40.9999C90.7889 40.9999 91.4285 41.6395 91.4285 42.4285C91.4285 43.2175 90.7889 43.8571 90 43.8571L62.7142 43.8571C61.9253 43.8571 61.2857 43.2175 61.2857 42.4285C61.2857 41.6395 61.9253 40.9999 62.7142 40.9999Z" fill="#6E49CB"/> +<path d="M62.7142 40.9999L90 40.9999C90.7889 40.9999 91.4285 41.6395 91.4285 42.4285C91.4285 43.2175 90.7889 43.8571 90 43.8571L62.7142 43.8571C61.9253 43.8571 61.2857 43.2175 61.2857 42.4285C61.2857 41.6395 61.9253 40.9999 62.7142 40.9999Z" fill="white" fill-opacity="0.6"/> +<path d="M69.8571 29.5714L91.5714 29.5714C92.3603 29.5714 92.9999 30.211 92.9999 31C92.9999 31.789 92.3603 32.4286 91.5714 32.4286L69.8571 32.4286C69.0681 32.4286 68.4285 31.789 68.4285 31C68.4285 30.211 69.0681 29.5714 69.8571 29.5714Z" fill="#6E49CB"/> +<path d="M69.8571 29.5714L91.5714 29.5714C92.3603 29.5714 92.9999 30.211 92.9999 31C92.9999 31.789 92.3603 32.4286 91.5714 32.4286L69.8571 32.4286C69.0681 32.4286 68.4285 31.789 68.4285 31C68.4285 30.211 69.0681 29.5714 69.8571 29.5714Z" fill="white" fill-opacity="0.8"/> +<circle cx="107.714" cy="38.8571" r="17.8571" stroke="#6E49CB" stroke-width="2.63203"/> +<circle cx="107.714" cy="38.8573" r="13.5714" fill="#6E49CB"/> +<circle cx="107.714" cy="38.8573" r="13.5714" fill="white" fill-opacity="0.9"/> +<path d="M111.431 35.0867L115.367 39.0232L111.431 42.9597C111.016 43.3744 110.344 43.3744 109.929 42.9597C109.515 42.545 109.515 41.8727 109.929 41.458L111.302 40.0851H101.123C100.537 40.0851 100.061 39.6097 100.061 39.0232C100.061 38.4367 100.537 37.9613 101.123 37.9613H111.302L109.929 36.5884C109.515 36.1737 109.515 35.5014 109.929 35.0867C110.344 34.672 111.016 34.672 111.431 35.0867Z" fill="#6E49CB"/> +</g> +<defs> +<clipPath id="clip0"> +<rect width="169" height="84" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/index.js b/app/assets/javascripts/projects/experiment_new_project_creation/index.js index 3715c52b6b9..06920a5ab19 100644 --- a/app/assets/javascripts/projects/experiment_new_project_creation/index.js +++ b/app/assets/javascripts/projects/experiment_new_project_creation/index.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import NewProjectCreationApp from './components/app.vue'; -export default function(el, props) { +export default function (el, props) { return new Vue({ el, components: { diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue index 4bf837faed1..7bb62cf4a73 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue @@ -1,37 +1,18 @@ <script> -import dateFormat from 'dateformat'; -import { GlColumnChart } from '@gitlab/ui/dist/charts'; -import { GlAlert, GlSkeletonLoader } from '@gitlab/ui'; -import { __, s__, sprintf } from '~/locale'; -import { getDateInPast } from '~/lib/utils/datetime_utility'; +import { GlAlert, GlTabs, GlTab } from '@gitlab/ui'; +import { s__ } from '~/locale'; import getPipelineCountByStatus from '../graphql/queries/get_pipeline_count_by_status.query.graphql'; import getProjectPipelineStatistics from '../graphql/queries/get_project_pipeline_statistics.query.graphql'; -import StatisticsList from './statistics_list.vue'; -import PipelinesAreaChart from './pipelines_area_chart.vue'; +import PipelineCharts from './pipeline_charts.vue'; + import { - CHART_CONTAINER_HEIGHT, - CHART_DATE_FORMAT, DEFAULT, - INNER_CHART_HEIGHT, LOAD_ANALYTICS_FAILURE, LOAD_PIPELINES_FAILURE, - ONE_WEEK_AGO_DAYS, - ONE_MONTH_AGO_DAYS, PARSE_FAILURE, UNSUPPORTED_DATA, - X_AXIS_LABEL_ROTATION, - X_AXIS_TITLE_OFFSET, } from '../constants'; -const defaultCountValues = { - totalPipelines: { - count: 0, - }, - successfulPipelines: { - count: 0, - }, -}; - const defaultAnalyticsValues = { weekPipelinesTotals: [], weekPipelinesLabels: [], @@ -46,15 +27,29 @@ const defaultAnalyticsValues = { pipelineTimesValues: [], }; +const defaultCountValues = { + totalPipelines: { + count: 0, + }, + successfulPipelines: { + count: 0, + }, +}; + export default { components: { GlAlert, - GlColumnChart, - GlSkeletonLoader, - StatisticsList, - PipelinesAreaChart, + GlTabs, + GlTab, + PipelineCharts, + DeploymentFrequencyCharts: () => + import('ee_component/projects/pipelines/charts/components/deployment_frequency_charts.vue'), }, inject: { + shouldRenderDeploymentFrequencyCharts: { + type: Boolean, + default: false, + }, projectPath: { type: String, default: '', @@ -62,14 +57,10 @@ export default { }, data() { return { - counts: { - ...defaultCountValues, - }, - analytics: { - ...defaultAnalyticsValues, - }, showFailureAlert: false, failureType: null, + analytics: { ...defaultAnalyticsValues }, + counts: { ...defaultCountValues }, }; }, apollo: { @@ -127,47 +118,6 @@ export default { }; } }, - successRatio() { - const { successfulPipelines, failedPipelines } = this.counts; - const successfulCount = successfulPipelines?.count; - const failedCount = failedPipelines?.count; - const ratio = (successfulCount / (successfulCount + failedCount)) * 100; - - return failedCount === 0 ? 100 : ratio; - }, - formattedCounts() { - const { - totalPipelines, - successfulPipelines, - failedPipelines, - totalPipelineDuration, - } = this.counts; - - return { - total: totalPipelines?.count, - success: successfulPipelines?.count, - failed: failedPipelines?.count, - successRatio: this.successRatio, - totalDuration: totalPipelineDuration, - }; - }, - areaCharts() { - const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles; - let areaChartsData = []; - - try { - areaChartsData = [ - this.buildAreaChartData(lastWeek, this.lastWeekChartData), - this.buildAreaChartData(lastMonth, this.lastMonthChartData), - this.buildAreaChartData(lastYear, this.lastYearChartData), - ]; - } catch { - areaChartsData = []; - this.reportFailure(PARSE_FAILURE); - } - - return areaChartsData; - }, lastWeekChartData() { return { labels: this.analytics.weekPipelinesLabels, @@ -189,39 +139,32 @@ export default { success: this.analytics.yearPipelinesSuccessful, }; }, - timesChartTransformedData() { - return [ - { - name: 'full', - data: this.mergeLabelsAndValues( - this.analytics.pipelineTimesLabels, - this.analytics.pipelineTimesValues, - ), - }, - ]; + timesChartData() { + return { + labels: this.analytics.pipelineTimesLabels, + values: this.analytics.pipelineTimesValues, + }; }, - }, - methods: { - mergeLabelsAndValues(labels, values) { - return labels.map((label, index) => [label, values[index]]); + successRatio() { + const { successfulPipelines, failedPipelines } = this.counts; + const successfulCount = successfulPipelines?.count; + const failedCount = failedPipelines?.count; + const ratio = (successfulCount / (successfulCount + failedCount)) * 100; + + return failedCount === 0 ? 100 : ratio; }, - buildAreaChartData(title, data) { - const { labels, totals, success } = data; + formattedCounts() { + const { totalPipelines, successfulPipelines, failedPipelines } = this.counts; return { - title, - data: [ - { - name: 'all', - data: this.mergeLabelsAndValues(labels, totals), - }, - { - name: 'success', - data: this.mergeLabelsAndValues(labels, success), - }, - ], + total: totalPipelines?.count, + success: successfulPipelines?.count, + failed: failedPipelines?.count, + successRatio: this.successRatio, }; }, + }, + methods: { hideAlert() { this.showFailureAlert = false; }, @@ -230,16 +173,6 @@ export default { this.failureType = type; }, }, - chartContainerHeight: CHART_CONTAINER_HEIGHT, - timesChartOptions: { - height: INNER_CHART_HEIGHT, - xAxis: { - axisLabel: { - rotate: X_AXIS_LABEL_ROTATION, - }, - nameGap: X_AXIS_TITLE_OFFSET, - }, - }, errorTexts: { [LOAD_ANALYTICS_FAILURE]: s__( 'PipelineCharts|An error has ocurred when retrieving the analytics data', @@ -250,60 +183,38 @@ export default { [PARSE_FAILURE]: s__('PipelineCharts|There was an error parsing the data for the charts.'), [DEFAULT]: s__('PipelineCharts|An unknown error occurred while processing CI/CD analytics.'), }, - get chartTitles() { - const today = dateFormat(new Date(), CHART_DATE_FORMAT); - const pastDate = timeScale => - dateFormat(getDateInPast(new Date(), timeScale), CHART_DATE_FORMAT); - return { - lastWeek: sprintf(__('Pipelines for last week (%{oneWeekAgo} - %{today})'), { - oneWeekAgo: pastDate(ONE_WEEK_AGO_DAYS), - today, - }), - lastMonth: sprintf(__('Pipelines for last month (%{oneMonthAgo} - %{today})'), { - oneMonthAgo: pastDate(ONE_MONTH_AGO_DAYS), - today, - }), - lastYear: __('Pipelines for last year'), - }; - }, }; </script> <template> <div> - <gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="hideAlert"> - {{ failure.text }} - </gl-alert> - <div class="gl-mb-3"> - <h3>{{ s__('PipelineCharts|CI / CD Analytics') }}</h3> - </div> - <h4 class="gl-my-4">{{ s__('PipelineCharts|Overall statistics') }}</h4> - <div class="row"> - <div class="col-md-6"> - <gl-skeleton-loader v-if="$apollo.queries.counts.loading" :lines="5" /> - <statistics-list v-else :counts="formattedCounts" /> - </div> - <div class="col-md-6"> - <strong> - {{ __('Duration for the last 30 commits') }} - </strong> - <gl-column-chart - :height="$options.chartContainerHeight" - :option="$options.timesChartOptions" - :bars="timesChartTransformedData" - :y-axis-title="__('Minutes')" - :x-axis-title="__('Commit')" - x-axis-type="category" + <gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="hideAlert">{{ + failure.text + }}</gl-alert> + <gl-tabs v-if="shouldRenderDeploymentFrequencyCharts"> + <gl-tab :title="__('Pipelines')"> + <pipeline-charts + :counts="formattedCounts" + :last-week="lastWeekChartData" + :last-month="lastMonthChartData" + :last-year="lastYearChartData" + :times-chart="timesChartData" + :loading="$apollo.queries.counts.loading" + @report-failure="reportFailure" /> - </div> - </div> - <hr /> - <h4 class="gl-my-4">{{ __('Pipelines charts') }}</h4> - <pipelines-area-chart - v-for="(chart, index) in areaCharts" - :key="index" - :chart-data="chart.data" - > - {{ chart.title }} - </pipelines-area-chart> + </gl-tab> + <gl-tab :title="__('Deployments')"> + <deployment-frequency-charts /> + </gl-tab> + </gl-tabs> + <pipeline-charts + v-else + :counts="formattedCounts" + :last-week="lastWeekChartData" + :last-month="lastMonthChartData" + :last-year="lastYearChartData" + :times-chart="timesChartData" + :loading="$apollo.queries.counts.loading" + @report-failure="reportFailure" + /> </div> </template> diff --git a/app/assets/javascripts/projects/pipelines/charts/components/pipelines_area_chart.vue b/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue index d726196aadf..3590e2c4632 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/pipelines_area_chart.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue @@ -1,10 +1,10 @@ <script> import { GlAreaChart } from '@gitlab/ui/dist/charts'; -import { s__ } from '~/locale'; import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue'; import { CHART_CONTAINER_HEIGHT } from '../constants'; export default { + name: 'CiCdAnalyticsAreaChart', components: { GlAreaChart, ResizableChartContainer, @@ -14,14 +14,9 @@ export default { type: Array, required: true, }, - }, - areaChartOptions: { - xAxis: { - name: s__('Pipeline|Date'), - type: 'category', - }, - yAxis: { - name: s__('Pipeline|Pipelines'), + areaChartOptions: { + type: Object, + required: true, }, }, chartContainerHeight: CHART_CONTAINER_HEIGHT, @@ -39,7 +34,7 @@ export default { :height="$options.chartContainerHeight" :data="chartData" :include-legend-avg-max="false" - :option="$options.areaChartOptions" + :option="areaChartOptions" /> </resizable-chart-container> </div> diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app_legacy.vue b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue index c6e2b2e1140..bec4ab407f0 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/app_legacy.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue @@ -1,66 +1,81 @@ <script> import dateFormat from 'dateformat'; import { GlColumnChart } from '@gitlab/ui/dist/charts'; -import { __, sprintf } from '~/locale'; +import { GlSkeletonLoader } from '@gitlab/ui'; +import { __, s__, sprintf } from '~/locale'; import { getDateInPast } from '~/lib/utils/datetime_utility'; -import StatisticsList from './statistics_list.vue'; -import PipelinesAreaChart from './pipelines_area_chart.vue'; import { CHART_CONTAINER_HEIGHT, - INNER_CHART_HEIGHT, - X_AXIS_LABEL_ROTATION, - X_AXIS_TITLE_OFFSET, CHART_DATE_FORMAT, + INNER_CHART_HEIGHT, ONE_WEEK_AGO_DAYS, ONE_MONTH_AGO_DAYS, + X_AXIS_LABEL_ROTATION, + X_AXIS_TITLE_OFFSET, + PARSE_FAILURE, } from '../constants'; +import StatisticsList from './statistics_list.vue'; +import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue'; export default { components: { - StatisticsList, GlColumnChart, - PipelinesAreaChart, + GlSkeletonLoader, + StatisticsList, + CiCdAnalyticsAreaChart, }, props: { counts: { - type: Object, required: true, - }, - timesChartData: { type: Object, - required: true, }, - lastWeekChartData: { - type: Object, - required: true, + loading: { + required: false, + default: false, + type: Boolean, }, - lastMonthChartData: { + lastWeek: { + required: true, type: Object, + }, + lastMonth: { required: true, + type: Object, }, - lastYearChartData: { + lastYear: { + required: true, type: Object, + }, + timesChart: { required: true, + type: Object, }, }, - data() { - return { - timesChartTransformedData: [ - { - name: 'full', - data: this.mergeLabelsAndValues(this.timesChartData.labels, this.timesChartData.values), - }, - ], - }; - }, computed: { areaCharts() { const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles; + const charts = [ + { title: lastWeek, data: this.lastWeek }, + { title: lastMonth, data: this.lastMonth }, + { title: lastYear, data: this.lastYear }, + ]; + let areaChartsData = []; + + try { + areaChartsData = charts.map(this.buildAreaChartData); + } catch { + areaChartsData = []; + this.vm.$emit('report-failure', PARSE_FAILURE); + } + return areaChartsData; + }, + timesChartTransformedData() { return [ - this.buildAreaChartData(lastWeek, this.lastWeekChartData), - this.buildAreaChartData(lastMonth, this.lastMonthChartData), - this.buildAreaChartData(lastYear, this.lastYearChartData), + { + name: 'full', + data: this.mergeLabelsAndValues(this.timesChart.labels, this.timesChart.values), + }, ]; }, }, @@ -68,7 +83,7 @@ export default { mergeLabelsAndValues(labels, values) { return labels.map((label, index) => [label, values[index]]); }, - buildAreaChartData(title, data) { + buildAreaChartData({ title, data }) { const { labels, totals, success } = data; return { @@ -96,9 +111,18 @@ export default { nameGap: X_AXIS_TITLE_OFFSET, }, }, + areaChartOptions: { + xAxis: { + name: s__('Pipeline|Date'), + type: 'category', + }, + yAxis: { + name: s__('Pipeline|Pipelines'), + }, + }, get chartTitles() { const today = dateFormat(new Date(), CHART_DATE_FORMAT); - const pastDate = timeScale => + const pastDate = (timeScale) => dateFormat(getDateInPast(new Date(), timeScale), CHART_DATE_FORMAT); return { lastWeek: sprintf(__('Pipelines for last week (%{oneWeekAgo} - %{today})'), { @@ -116,18 +140,17 @@ export default { </script> <template> <div> - <div class="mb-3"> + <div class="gl-mb-3"> <h3>{{ s__('PipelineCharts|CI / CD Analytics') }}</h3> </div> - <h4 class="my-4">{{ s__('PipelineCharts|Overall statistics') }}</h4> + <h4 class="gl-my-4">{{ s__('PipelineCharts|Overall statistics') }}</h4> <div class="row"> <div class="col-md-6"> - <statistics-list :counts="counts" /> + <gl-skeleton-loader v-if="loading" :lines="5" /> + <statistics-list v-else :counts="counts" /> </div> - <div class="col-md-6"> - <strong> - {{ __('Duration for the last 30 commits') }} - </strong> + <div v-if="!loading" class="col-md-6"> + <strong>{{ __('Duration for the last 30 commits') }}</strong> <gl-column-chart :height="$options.chartContainerHeight" :option="$options.timesChartOptions" @@ -138,14 +161,16 @@ export default { /> </div> </div> - <hr /> - <h4 class="my-4">{{ __('Pipelines charts') }}</h4> - <pipelines-area-chart - v-for="(chart, index) in areaCharts" - :key="index" - :chart-data="chart.data" - > - {{ chart.title }} - </pipelines-area-chart> + <template v-if="!loading"> + <hr /> + <h4 class="gl-my-4">{{ __('Pipelines charts') }}</h4> + <ci-cd-analytics-area-chart + v-for="(chart, index) in areaCharts" + :key="index" + :chart-data="chart.data" + :area-chart-options="$options.areaChartOptions" + >{{ chart.title }}</ci-cd-analytics-area-chart + > + </template> </div> </template> diff --git a/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue b/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue index 94cecd2e479..7bc3b787f75 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue @@ -1,5 +1,4 @@ <script> -import { formatTime } from '~/lib/utils/datetime_utility'; import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format'; import { s__, n__ } from '~/locale'; @@ -13,9 +12,6 @@ export default { }, }, computed: { - totalDuration() { - return formatTime(this.counts.totalDuration); - }, statistics() { const formatter = getFormatter(SUPPORTED_FORMATS.percentHundred); @@ -36,10 +32,6 @@ export default { title: s__('PipelineCharts|Success ratio:'), value: formatter(this.counts.successRatio, defaultPrecision), }, - { - title: s__('PipelineCharts|Total duration:'), - value: this.totalDuration, - }, ]; }, }, diff --git a/app/assets/javascripts/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql b/app/assets/javascripts/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql index eb0dbf8dd16..d68df689f5f 100644 --- a/app/assets/javascripts/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql +++ b/app/assets/javascripts/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql @@ -9,6 +9,5 @@ query getPipelineCountByStatus($projectPath: ID!) { failedPipelines: pipelines(status: FAILED) { count } - totalPipelineDuration } } diff --git a/app/assets/javascripts/projects/pipelines/charts/index.js b/app/assets/javascripts/projects/pipelines/charts/index.js index f6e79f0ab51..7e746423b6a 100644 --- a/app/assets/javascripts/projects/pipelines/charts/index.js +++ b/app/assets/javascripts/projects/pipelines/charts/index.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; -import ProjectPipelinesChartsLegacy from './components/app_legacy.vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; import ProjectPipelinesCharts from './components/app.vue'; Vue.use(VueApollo); @@ -10,99 +10,25 @@ const apolloProvider = new VueApollo({ defaultClient: createDefaultClient(), }); -const mountPipelineChartsApp = el => { - // Not all of the values will be defined since some them will be - // empty depending on the value of the graphql_pipeline_analytics - // feature flag, once the rollout of the feature flag is completed - // the undefined values will be deleted - const { - countsFailed, - countsSuccess, - countsTotal, - countsTotalDuration, - successRatio, - timesChartLabels, - timesChartValues, - lastWeekChartLabels, - lastWeekChartTotals, - lastWeekChartSuccess, - lastMonthChartLabels, - lastMonthChartTotals, - lastMonthChartSuccess, - lastYearChartLabels, - lastYearChartTotals, - lastYearChartSuccess, - projectPath, - } = el.dataset; +const mountPipelineChartsApp = (el) => { + const { projectPath } = el.dataset; - const parseAreaChartData = (labels, totals, success) => { - let parsedData = {}; - - try { - parsedData = { - labels: JSON.parse(labels), - totals: JSON.parse(totals), - success: JSON.parse(success), - }; - } catch { - parsedData = {}; - } - - return parsedData; - }; - - if (gon?.features?.graphqlPipelineAnalytics) { - return new Vue({ - el, - name: 'ProjectPipelinesChartsApp', - components: { - ProjectPipelinesCharts, - }, - apolloProvider, - provide: { - projectPath, - }, - render: createElement => createElement(ProjectPipelinesCharts, {}), - }); - } + const shouldRenderDeploymentFrequencyCharts = parseBoolean( + el.dataset.shouldRenderDeploymentFrequencyCharts, + ); return new Vue({ el, - name: 'ProjectPipelinesChartsAppLegacy', + name: 'ProjectPipelinesChartsApp', components: { - ProjectPipelinesChartsLegacy, + ProjectPipelinesCharts, + }, + apolloProvider, + provide: { + projectPath, + shouldRenderDeploymentFrequencyCharts, }, - render: createElement => - createElement(ProjectPipelinesChartsLegacy, { - props: { - counts: { - failed: countsFailed, - success: countsSuccess, - total: countsTotal, - successRatio, - totalDuration: countsTotalDuration, - }, - timesChartData: { - labels: JSON.parse(timesChartLabels), - values: JSON.parse(timesChartValues), - }, - lastWeekChartData: parseAreaChartData( - lastWeekChartLabels, - lastWeekChartTotals, - lastWeekChartSuccess, - ), - lastMonthChartData: parseAreaChartData( - lastMonthChartLabels, - lastMonthChartTotals, - lastMonthChartSuccess, - ), - lastYearChartData: parseAreaChartData( - lastYearChartLabels, - lastYearChartTotals, - lastYearChartSuccess, - ), - }, - }), + render: (createElement) => createElement(ProjectPipelinesCharts, {}), }); }; diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index d54a48cc444..e3ba84102a8 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -39,7 +39,7 @@ const setProjectNamePathHandlers = ($projectNameInput, $projectPathInput) => { }); }; -const deriveProjectPathFromUrl = $projectImportUrl => { +const deriveProjectPathFromUrl = ($projectImportUrl) => { const $currentProjectName = $projectImportUrl .parents('.toggle-import-form') .find('#project_name'); @@ -89,11 +89,9 @@ const bindEvents = () => { return; } - $('.how_to_import_link').on('click', e => { + $('.how_to_import_link').on('click', (e) => { e.preventDefault(); - $(e.currentTarget) - .next('.modal') - .show(); + $(e.currentTarget).next('.modal').show(); }); $('.modal-header .close').on('click', () => { @@ -114,7 +112,7 @@ const bindEvents = () => { $pushNewProjectTipTrigger .removeAttr('rel') .removeAttr('target') - .on('click', e => { + .on('click', (e) => { e.preventDefault(); }) .popover({ @@ -124,7 +122,7 @@ const bindEvents = () => { content: $('.push-new-project-tip-template').html(), }) .on('shown.bs.popover', () => { - $(document).on('click.popover touchstart.popover', event => { + $(document).on('click.popover touchstart.popover', (event) => { if ($(event.target).closest('.popover').length === 0) { $pushNewProjectTipTrigger.trigger('click'); } @@ -151,10 +149,7 @@ const bindEvents = () => { const selectedTemplate = DEFAULT_PROJECT_TEMPLATES[value]; $selectedTemplateText.text(selectedTemplate.text); - $(selectedTemplate.icon) - .clone() - .addClass('d-block') - .appendTo($selectedIcon); + $(selectedTemplate.icon).clone().addClass('d-block').appendTo($selectedIcon); const $activeTabProjectName = $('.tab-pane.active #project_name'); const $activeTabProjectPath = $('.tab-pane.active #project_path'); diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js index cb4fd5265da..a62b5d423de 100644 --- a/app/assets/javascripts/projects/settings/access_dropdown.js +++ b/app/assets/javascripts/projects/settings/access_dropdown.js @@ -25,7 +25,7 @@ export default class AccessDropdown { this.setSelectedItems([]); this.persistPreselectedItems(); - this.noOneObj = this.accessLevelsData.find(level => level.id === ACCESS_LEVEL_NONE); + this.noOneObj = this.accessLevelsData.find((level) => level.id === ACCESS_LEVEL_NONE); this.initDropdown(); } @@ -45,7 +45,7 @@ export default class AccessDropdown { onHide(); } }, - clicked: options => { + clicked: (options) => { const { $el, e } = options; const item = options.selectedObj; const fossWithMergeAccess = !this.hasLicense && this.accessLevel === ACCESS_LEVELS.MERGE; @@ -56,7 +56,7 @@ export default class AccessDropdown { // We're not multiselecting quite yet in "Merge" access dropdown, on FOSS: // remove all preselected items before selecting this item // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37499 - this.accessLevelsData.forEach(level => { + this.accessLevelsData.forEach((level) => { this.removeSelectedItem(level); }); } @@ -65,7 +65,7 @@ export default class AccessDropdown { if (this.noOneObj) { if (item.id === this.noOneObj.id && !fossWithMergeAccess) { // remove all others selected items - this.accessLevelsData.forEach(level => { + this.accessLevelsData.forEach((level) => { if (level.id !== item.id) { this.removeSelectedItem(level); } @@ -109,7 +109,7 @@ export default class AccessDropdown { return; } - const persistedItems = itemsToPreselect.map(item => { + const persistedItems = itemsToPreselect.map((item) => { const persistedItem = { ...item }; persistedItem.persisted = true; return persistedItem; @@ -123,7 +123,7 @@ export default class AccessDropdown { } getSelectedItems() { - return this.items.filter(item => !item._destroy); + return this.items.filter((item) => !item._destroy); } getAllSelectedItems() { @@ -134,7 +134,7 @@ export default class AccessDropdown { getInputData() { const selectedItems = this.getAllSelectedItems(); - const accessLevels = selectedItems.map(item => { + const accessLevels = selectedItems.map((item) => { const obj = {}; if (typeof item.id !== 'undefined') { @@ -288,12 +288,14 @@ export default class AccessDropdown { $dropdownToggleText.removeClass('is-default'); if (currentItems.length === 1 && currentItems[0].type === LEVEL_TYPES.ROLE) { - const roleData = this.accessLevelsData.find(data => data.id === currentItems[0].access_level); + const roleData = this.accessLevelsData.find( + (data) => data.id === currentItems[0].access_level, + ); return roleData.text; } const labelPieces = []; - const counts = countBy(currentItems, item => item.type); + const counts = countBy(currentItems, (item) => item.type); if (counts[LEVEL_TYPES.ROLE] > 0) { labelPieces.push(n__('1 role', '%d roles', counts[LEVEL_TYPES.ROLE])); @@ -336,7 +338,7 @@ export default class AccessDropdown { }); } else { this.getDeployKeys(query) - .then(deployKeysResponse => callback(this.consolidateData(deployKeysResponse.data))) + .then((deployKeysResponse) => callback(this.consolidateData(deployKeysResponse.data))) .catch(() => createFlash({ message: __('Failed to load deploy keys.') })); } } @@ -365,7 +367,7 @@ export default class AccessDropdown { /* * Build roles */ - const roles = this.accessLevelsData.map(level => { + const roles = this.accessLevelsData.map((level) => { /* eslint-disable no-param-reassign */ // This re-assignment is intentional as // level.type property is being used in removeSelectedItem() @@ -389,7 +391,7 @@ export default class AccessDropdown { /* * Build groups */ - const groups = groupsResponse.map(group => ({ + const groups = groupsResponse.map((group) => ({ ...group, type: LEVEL_TYPES.GROUP, })); @@ -398,8 +400,8 @@ export default class AccessDropdown { * Build users */ const users = selectedItems - .filter(item => item.type === LEVEL_TYPES.USER) - .map(item => { + .filter((item) => item.type === LEVEL_TYPES.USER) + .map((item) => { // Save identifiers for easy-checking more later map.push(LEVEL_TYPES.USER + item.user_id); @@ -414,7 +416,7 @@ export default class AccessDropdown { // Has to be checked against server response // because the selected item can be in filter results - usersResponse.forEach(response => { + usersResponse.forEach((response) => { // Add is it has not been added if (map.indexOf(LEVEL_TYPES.USER + response.id) === -1) { const user = { ...response }; @@ -444,7 +446,7 @@ export default class AccessDropdown { } if (this.deployKeysOnProtectedBranchesEnabled) { - const deployKeys = deployKeysResponse.map(response => { + const deployKeys = deployKeysResponse.map((response) => { const { id, fingerprint, diff --git a/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue b/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue index a4924033c1e..51281def7d0 100644 --- a/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue +++ b/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue @@ -46,7 +46,7 @@ export default { this.isLoading = false; this.isSharedRunnerEnabled = !this.isSharedRunnerEnabled; }) - .catch(error => { + .catch((error) => { this.isLoading = false; this.errorMessage = error.response?.data?.error || DEFAULT_ERROR_MESSAGE; }); diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue index a07c57c42cb..909f1afd9f6 100644 --- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue +++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue @@ -108,11 +108,11 @@ export default { .updateTemplate({ selectedTemplate, outgoingName, projectKey }, this.isEnabled) .then(({ data }) => { this.updatedCustomEmail = data?.service_desk_address; - this.showAlert(__('Changes were successfully made.'), 'success'); + this.showAlert(__('Changes saved.'), 'success'); }) - .catch(err => { + .catch((err) => { this.showAlert( - sprintf(__('An error occured while making the changes: %{error}'), { + sprintf(__('An error occured while saving changes: %{error}'), { error: err?.response?.data?.message, }), ); diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue index 2896cb491b5..a850374fc88 100644 --- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue +++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue @@ -103,7 +103,6 @@ export default { id="service-desk-checkbox" :value="isEnabled" class="d-inline-block align-middle mr-1" - label="Service desk" label-position="left" @change="onCheckboxToggle" /> @@ -113,7 +112,7 @@ export default { <div v-if="isEnabled" class="row mt-3"> <div class="col-md-9 mb-0"> <strong id="incoming-email-describer" class="d-block mb-1"> - {{ __('Forward external support email address to') }} + {{ __('Email address to use for Support Desk') }} </strong> <template v-if="email"> <div class="input-group"> @@ -137,7 +136,7 @@ export default { </div> </div> <span v-if="hasCustomEmail" class="form-text text-muted"> - <gl-sprintf :message="__('Emails sent to %{email} will still be supported')"> + <gl-sprintf :message="__('Emails sent to %{email} are also supported.')"> <template #email> <code>{{ incomingEmail }}</code> </template> @@ -156,9 +155,7 @@ export default { <input id="service-desk-project-suffix" v-model.trim="projectKey" class="form-control" /> <span class="form-text text-muted"> {{ - __( - 'Project name suffix is a user-defined string which will be appended to the project path, and will form the Service Desk email address.', - ) + __('A string appended to the project path to form the Service Desk email address.') }} </span> </template> @@ -176,7 +173,7 @@ export default { </label> <input id="service-desk-email-from-name" v-model.trim="outgoingName" class="form-control" /> <span class="form-text text-muted"> - {{ __('Emails sent from Service Desk will have this name') }} + {{ __('Emails sent from Service Desk have this name.') }} </span> <div class="gl-display-flex gl-justify-content-end"> <gl-button diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue index e582d5c3e47..0f01167988d 100644 --- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue +++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue @@ -63,7 +63,7 @@ export default { this.poll = new Poll({ resource: this.service, method: 'fetchData', - successCallback: response => this.successCallback(response), + successCallback: (response) => this.successCallback(response), errorCallback: this.errorCallback, }); @@ -83,10 +83,7 @@ export default { }); }, fetchPipelineCommitData() { - this.service - .fetchData() - .then(this.successCallback) - .catch(this.errorCallback); + this.service.fetchData().then(this.successCallback).catch(this.errorCallback); }, }, }; diff --git a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue b/app/assets/javascripts/prometheus_alerts/components/reset_key.vue index 6f60141d7ab..91de6d93e19 100644 --- a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue +++ b/app/assets/javascripts/prometheus_alerts/components/reset_key.vue @@ -61,7 +61,7 @@ export default { resetKey() { axios .post(this.changeKeyUrl) - .then(res => { + .then((res) => { this.authorizationKey = res.data.token; }) .catch(() => { diff --git a/app/assets/javascripts/prometheus_metrics/custom_metrics.js b/app/assets/javascripts/prometheus_metrics/custom_metrics.js index 36df7837785..e891b8bf3b6 100644 --- a/app/assets/javascripts/prometheus_metrics/custom_metrics.js +++ b/app/assets/javascripts/prometheus_metrics/custom_metrics.js @@ -65,12 +65,12 @@ export default class CustomMetrics extends PrometheusMetrics { // eslint-disable-next-line class-methods-use-this setHidden(els) { - els.forEach(el => el.addClass('hidden')); + els.forEach((el) => el.addClass('hidden')); } setVisible(...els) { - this.setHidden(this.$els.filter(el => !els.includes(el))); - els.forEach(el => el.removeClass('hidden')); + this.setHidden(this.$els.filter((el) => !els.includes(el))); + els.forEach((el) => el.removeClass('hidden')); } showMonitoringCustomMetricsPanelState(stateName) { @@ -98,14 +98,14 @@ export default class CustomMetrics extends PrometheusMetrics { } populateCustomMetrics() { - const capitalizeGroup = metric => ({ + const capitalizeGroup = (metric) => ({ ...metric, group: capitalizeFirstCharacter(metric.group), }); const sortedMetrics = sortBy(this.customMetrics.map(capitalizeGroup), ['group', 'title']); - sortedMetrics.forEach(metric => { + sortedMetrics.forEach((metric) => { this.$monitoredCustomMetricsList.append(CustomMetrics.customMetricTemplate(metric)); }); @@ -145,7 +145,7 @@ export default class CustomMetrics extends PrometheusMetrics { this.populateCustomMetrics(customMetrics.data.metrics); } }) - .catch(customMetricError => { + .catch((customMetricError) => { this.showFlashMessage(customMetricError); this.showMonitoringCustomMetricsPanelState(PANEL_STATE.EMPTY); }); diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js index bb9689f09a1..57f9cec9682 100644 --- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js +++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js @@ -26,8 +26,8 @@ export default class PrometheusMetrics { this.activeMetricsEndpoint = this.$monitoredMetricsPanel.data('activeMetrics'); this.helpMetricsPath = this.$monitoredMetricsPanel.data('metrics-help-path'); - this.$panelToggleRight.on('click', e => this.handlePanelToggle(e)); - this.$panelToggleDown.on('click', e => this.handlePanelToggle(e)); + this.$panelToggleRight.on('click', (e) => this.handlePanelToggle(e)); + this.$panelToggleDown.on('click', (e) => this.handlePanelToggle(e)); } init() { @@ -72,7 +72,7 @@ export default class PrometheusMetrics { let totalMissingEnvVarMetrics = 0; let totalExporters = 0; - metrics.forEach(metric => { + metrics.forEach((metric) => { if (metric.active_metrics > 0) { totalExporters += 1; this.$monitoredMetricsList.append( @@ -137,7 +137,7 @@ export default class PrometheusMetrics { }) .catch(stop); }) - .then(res => { + .then((res) => { if (res && res.data && res.data.length) { this.populateActiveMetrics(res.data); } else { diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js index 19f6666fd52..a5c7b18f709 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_create.js +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js @@ -90,12 +90,12 @@ export default class ProtectedBranchCreate { }, }; - Object.keys(ACCESS_LEVELS).forEach(level => { + Object.keys(ACCESS_LEVELS).forEach((level) => { const accessLevel = ACCESS_LEVELS[level]; const selectedItems = this[`${accessLevel}_dropdown`].getSelectedItems(); const levelAttributes = []; - selectedItems.forEach(item => { + selectedItems.forEach((item) => { if (item.type === LEVEL_TYPES.USER) { levelAttributes.push({ user_id: item.user_id, diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js index 1f079123081..f5f27b67c71 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js @@ -108,7 +108,7 @@ export default class ProtectedBranchEdit { .then(({ data }) => { this.hasChanges = false; - Object.keys(ACCESS_LEVELS).forEach(level => { + Object.keys(ACCESS_LEVELS).forEach((level) => { const accessLevelName = ACCESS_LEVELS[level]; // The data coming from server will be the new persisted *state* for each dropdown @@ -125,7 +125,7 @@ export default class ProtectedBranchEdit { } setSelectedItemsToDropdown(items = [], dropdownName) { - const itemsToAdd = items.map(currentItem => { + const itemsToAdd = items.map((currentItem) => { if (currentItem.user_id) { // Do this only for users for now // get the current data for selected items diff --git a/app/assets/javascripts/read_more.js b/app/assets/javascripts/read_more.js index d2d1ac8c76a..769782607b8 100644 --- a/app/assets/javascripts/read_more.js +++ b/app/assets/javascripts/read_more.js @@ -22,7 +22,7 @@ export default function initReadMore(triggerSelector = '.js-read-more-trigger') if (!triggerEls) return; - triggerEls.forEach(triggerEl => { + triggerEls.forEach((triggerEl) => { const targetEl = triggerEl.previousElementSibling; if (!targetEl) { @@ -31,7 +31,7 @@ export default function initReadMore(triggerSelector = '.js-read-more-trigger') triggerEl.addEventListener( 'click', - e => { + (e) => { targetEl.classList.add('is-expanded'); e.target.remove(); }, diff --git a/app/assets/javascripts/ref/components/ref_results_section.vue b/app/assets/javascripts/ref/components/ref_results_section.vue index dc74f86fd70..87ce4f1a49c 100644 --- a/app/assets/javascripts/ref/components/ref_results_section.vue +++ b/app/assets/javascripts/ref/components/ref_results_section.vue @@ -39,7 +39,7 @@ export default { items: { type: Array, required: true, - validator: items => Array.isArray(items) && items.every(item => item.name), + validator: (items) => Array.isArray(items) && items.every((item) => item.name), }, /** diff --git a/app/assets/javascripts/ref/components/ref_selector.vue b/app/assets/javascripts/ref/components/ref_selector.vue index 0084450c9b0..20aec3e12be 100644 --- a/app/assets/javascripts/ref/components/ref_selector.vue +++ b/app/assets/javascripts/ref/components/ref_selector.vue @@ -50,9 +50,9 @@ export default { }, computed: { ...mapState({ - matches: state => state.matches, - lastQuery: state => state.query, - selectedRef: state => state.selectedRef, + matches: (state) => state.matches, + lastQuery: (state) => state.query, + selectedRef: (state) => state.selectedRef, }), ...mapGetters(['isLoading', 'isQueryPossiblyASha']), i18n() { diff --git a/app/assets/javascripts/ref/stores/actions.js b/app/assets/javascripts/ref/stores/actions.js index 8fcc99cef38..d9bdd64ace5 100644 --- a/app/assets/javascripts/ref/stores/actions.js +++ b/app/assets/javascripts/ref/stores/actions.js @@ -18,10 +18,10 @@ export const searchBranches = ({ commit, state }) => { commit(types.REQUEST_START); Api.branches(state.projectId, state.query) - .then(response => { + .then((response) => { commit(types.RECEIVE_BRANCHES_SUCCESS, response); }) - .catch(error => { + .catch((error) => { commit(types.RECEIVE_BRANCHES_ERROR, error); }) .finally(() => { @@ -33,10 +33,10 @@ export const searchTags = ({ commit, state }) => { commit(types.REQUEST_START); Api.tags(state.projectId, state.query) - .then(response => { + .then((response) => { commit(types.RECEIVE_TAGS_SUCCESS, response); }) - .catch(error => { + .catch((error) => { commit(types.RECEIVE_TAGS_ERROR, error); }) .finally(() => { @@ -50,10 +50,10 @@ export const searchCommits = ({ commit, state, getters }) => { commit(types.REQUEST_START); Api.commit(state.projectId, state.query) - .then(response => { + .then((response) => { commit(types.RECEIVE_COMMITS_SUCCESS, response); }) - .catch(error => { + .catch((error) => { commit(types.RECEIVE_COMMITS_ERROR, error); }) .finally(() => { diff --git a/app/assets/javascripts/ref/stores/mutations.js b/app/assets/javascripts/ref/stores/mutations.js index 73f9d7ee487..75026a40175 100644 --- a/app/assets/javascripts/ref/stores/mutations.js +++ b/app/assets/javascripts/ref/stores/mutations.js @@ -23,7 +23,7 @@ export default { [types.RECEIVE_BRANCHES_SUCCESS](state, response) { state.matches.branches = { - list: convertObjectPropsToCamelCase(response.data).map(b => ({ + list: convertObjectPropsToCamelCase(response.data).map((b) => ({ name: b.name, default: b.default, })), @@ -41,7 +41,7 @@ export default { [types.RECEIVE_TAGS_SUCCESS](state, response) { state.matches.tags = { - list: convertObjectPropsToCamelCase(response.data).map(b => ({ + list: convertObjectPropsToCamelCase(response.data).map((b) => ({ name: b.name, })), totalCount: parseInt(response.headers[X_TOTAL_HEADER], 10), diff --git a/app/assets/javascripts/ref_select_dropdown.js b/app/assets/javascripts/ref_select_dropdown.js index af8729c1d08..c283fb1ea08 100644 --- a/app/assets/javascripts/ref_select_dropdown.js +++ b/app/assets/javascripts/ref_select_dropdown.js @@ -31,7 +31,7 @@ class RefSelectDropdown { const $fieldInput = $(`input[name="${$dropdownButton.data('fieldName')}"]`, $dropdownContainer); const $filterInput = $('input[type="search"]', $dropdownContainer); - $filterInput.on('keyup', e => { + $filterInput.on('keyup', (e) => { const keyCode = e.keyCode || e.which; if (keyCode !== 13) return; diff --git a/app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue b/app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue index 8bdf043a106..56d2ff86fb7 100644 --- a/app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue +++ b/app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue @@ -60,7 +60,7 @@ export default { @dismiss="$emit('change', null)" > <gl-sprintf :message="deleteAlertConfig.message"> - <template #docLink="{content}"> + <template #docLink="{ content }"> <gl-link :href="garbageCollectionHelpPagePath" target="_blank"> {{ content }} </gl-link> diff --git a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue index 3eeb7b29386..ed02aa264ed 100644 --- a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue +++ b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue @@ -1,12 +1,29 @@ <script> import { GlSprintf } from '@gitlab/ui'; -import { sprintf } from '~/locale'; +import { sprintf, n__ } from '~/locale'; import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; -import { DETAILS_PAGE_TITLE, UPDATED_AT } from '../../constants/index'; +import { + DETAILS_PAGE_TITLE, + UPDATED_AT, + CLEANUP_UNSCHEDULED_TEXT, + CLEANUP_SCHEDULED_TEXT, + CLEANUP_ONGOING_TEXT, + CLEANUP_UNFINISHED_TEXT, + CLEANUP_DISABLED_TEXT, + CLEANUP_SCHEDULED_TOOLTIP, + CLEANUP_ONGOING_TOOLTIP, + CLEANUP_UNFINISHED_TOOLTIP, + CLEANUP_DISABLED_TOOLTIP, + UNFINISHED_STATUS, + UNSCHEDULED_STATUS, + SCHEDULED_STATUS, + ONGOING_STATUS, +} from '../../constants/index'; export default { + name: 'DetailsHeader', components: { GlSprintf, TitleArea, MetadataItem }, mixins: [timeagoMixin], props: { @@ -14,6 +31,11 @@ export default { type: Object, required: true, }, + metadataLoading: { + type: Boolean, + required: false, + default: false, + }, }, computed: { visibilityIcon() { @@ -25,6 +47,24 @@ export default { updatedText() { return sprintf(UPDATED_AT, { time: this.timeAgo }); }, + tagCountText() { + return n__('%d tag', '%d tags', this.image.tagsCount); + }, + cleanupTextAndTooltip() { + if (!this.image.project.containerExpirationPolicy?.enabled) { + return { text: CLEANUP_DISABLED_TEXT, tooltip: CLEANUP_DISABLED_TOOLTIP }; + } + return { + [UNSCHEDULED_STATUS]: { + text: sprintf(CLEANUP_UNSCHEDULED_TEXT, { + time: this.timeFormatted(this.image.project.containerExpirationPolicy.nextRunAt), + }), + }, + [SCHEDULED_STATUS]: { text: CLEANUP_SCHEDULED_TEXT, tooltip: CLEANUP_SCHEDULED_TOOLTIP }, + [ONGOING_STATUS]: { text: CLEANUP_ONGOING_TEXT, tooltip: CLEANUP_ONGOING_TOOLTIP }, + [UNFINISHED_STATUS]: { text: CLEANUP_UNFINISHED_TEXT, tooltip: CLEANUP_UNFINISHED_TOOLTIP }, + }[this.image?.expirationPolicyCleanupStatus]; + }, }, i18n: { DETAILS_PAGE_TITLE, @@ -33,7 +73,7 @@ export default { </script> <template> - <title-area> + <title-area :metadata-loading="metadataLoading"> <template #title> <gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE"> <template #imageName> @@ -41,6 +81,20 @@ export default { </template> </gl-sprintf> </template> + <template #metadata-tags-count> + <metadata-item icon="tag" :text="tagCountText" data-testid="tags-count" /> + </template> + + <template #metadata-cleanup> + <metadata-item + icon="expire" + :text="cleanupTextAndTooltip.text" + :text-tooltip="cleanupTextAndTooltip.tooltip" + size="xl" + data-testid="cleanup" + /> + </template> + <template #metadata-updated> <metadata-item :icon="visibilityIcon" diff --git a/app/assets/javascripts/registry/explorer/components/details_page/partial_cleanup_alert.vue b/app/assets/javascripts/registry/explorer/components/details_page/partial_cleanup_alert.vue index d13d815a59e..12095655126 100644 --- a/app/assets/javascripts/registry/explorer/components/details_page/partial_cleanup_alert.vue +++ b/app/assets/javascripts/registry/explorer/components/details_page/partial_cleanup_alert.vue @@ -23,12 +23,12 @@ export default { <template> <gl-alert variant="warning" :title="$options.i18n.DELETE_ALERT_TITLE" @dismiss="$emit('dismiss')"> <gl-sprintf :message="$options.i18n.DELETE_ALERT_LINK_TEXT"> - <template #adminLink="{content}"> + <template #adminLink="{ content }"> <gl-link data-testid="run-link" :href="runCleanupPoliciesHelpPagePath" target="_blank">{{ content }}</gl-link> </template> - <template #docLink="{content}"> + <template #docLink="{ content }"> <gl-link data-testid="help-link" :href="cleanupPoliciesHelpPagePath" target="_blank">{{ content }}</gl-link> diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue index ad39a898e7b..1e0736c4a53 100644 --- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue +++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue @@ -4,6 +4,7 @@ import TagsListRow from './tags_list_row.vue'; import { REMOVE_TAGS_BUTTON_TITLE, TAGS_LIST_TITLE } from '../../constants/index'; export default { + name: 'TagsList', components: { GlButton, TagsListRow, @@ -31,10 +32,10 @@ export default { }, computed: { hasSelectedItems() { - return this.tags.some(tag => this.selectedItems[tag.name]); + return this.tags.some((tag) => this.selectedItems[tag.name]); }, showMultiDeleteButton() { - return this.tags.some(tag => tag.canDelete) && !this.isMobile; + return this.tags.some((tag) => tag.canDelete) && !this.isMobile; }, }, methods: { diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue index 3a5cccc7d08..2e4a489f2cb 100644 --- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue +++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue @@ -140,9 +140,7 @@ export default { <template #left-secondary> <span data-testid="size"> {{ formattedSize }} - <template v-if="formattedSize && layers" - >·</template - > + <template v-if="formattedSize && layers">·</template> {{ layers }} </span> </template> diff --git a/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue b/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue index 319666210d6..07ee3c6083b 100644 --- a/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue +++ b/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue @@ -19,8 +19,8 @@ export default { GlDropdown, CodeInstruction, }, - inject: ['config', 'dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand'], mixins: [Tracking.mixin({ label: trackingLabel })], + inject: ['config', 'dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand'], trackingLabel, i18n: { QUICK_START, diff --git a/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue b/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue index 26e9fee63af..a68c4de5aa6 100644 --- a/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue +++ b/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue @@ -3,12 +3,12 @@ import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui'; export default { name: 'GroupEmptyState', - inject: ['config'], components: { GlEmptyState, GlSprintf, GlLink, }, + inject: ['config'], }; </script> <template> @@ -25,7 +25,7 @@ export default { ) " > - <template #docLink="{content}"> + <template #docLink="{ content }"> <gl-link :href="config.helpPagePath" target="_blank">{{ content }}</gl-link> </template> </gl-sprintf> diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue b/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue index f8b3233438f..10ad99d5956 100644 --- a/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue +++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue @@ -13,6 +13,11 @@ export default { type: Array, required: true, }, + metadataLoading: { + type: Boolean, + default: false, + required: false, + }, pageInfo: { type: Object, required: true, @@ -33,6 +38,7 @@ export default { :key="index" :item="listItem" :first="index === 0" + :metadata-loading="metadataLoading" @delete="$emit('delete', $event)" /> <div class="gl-display-flex gl-justify-content-center"> diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue index 3fe61dc231a..264a3c27cde 100644 --- a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue +++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue @@ -1,5 +1,5 @@ <script> -import { GlTooltipDirective, GlIcon, GlSprintf } from '@gitlab/ui'; +import { GlTooltipDirective, GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui'; import { n__ } from '~/locale'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; @@ -18,13 +18,14 @@ import { } from '../../constants/index'; export default { - name: 'ImageListrow', + name: 'ImageListRow', components: { ClipboardButton, DeleteButton, GlSprintf, GlIcon, ListItem, + GlSkeletonLoader, }, directives: { GlTooltip: GlTooltipDirective, @@ -34,6 +35,11 @@ export default { type: Object, required: true, }, + metadataLoading: { + type: Boolean, + default: false, + required: false, + }, }, i18n: { LIST_DELETE_BUTTON_DISABLED, @@ -107,7 +113,11 @@ export default { /> </template> <template #left-secondary> - <span class="gl-display-flex gl-align-items-center" data-testid="tagsCount"> + <span + v-if="!metadataLoading" + class="gl-display-flex gl-align-items-center" + data-testid="tags-count" + > <gl-icon name="tag" class="gl-mr-2" /> <gl-sprintf :message="tagsCountText"> <template #count> @@ -115,6 +125,13 @@ export default { </template> </gl-sprintf> </span> + + <div v-else class="gl-w-full"> + <gl-skeleton-loader :width="900" :height="16" preserve-aspect-ratio="xMinYMax meet"> + <circle cx="6" cy="8" r="6" /> + <rect x="16" y="4" width="100" height="8" rx="4" /> + </gl-skeleton-loader> + </div> </template> <template #right-action> <delete-button diff --git a/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue b/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue index 5308b025cc0..5aa04419ca0 100644 --- a/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue +++ b/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue @@ -45,7 +45,7 @@ export default { <template #description> <p> <gl-sprintf :message="$options.i18n.introText"> - <template #docLink="{content}"> + <template #docLink="{ content }"> <gl-link :href="config.helpPagePath" target="_blank">{{ content }}</gl-link> </template> </gl-sprintf> @@ -53,10 +53,10 @@ export default { <h5>{{ $options.i18n.quickStart }}</h5> <p> <gl-sprintf :message="$options.i18n.notLoggedInMessage"> - <template #twofaDocLink="{content}"> + <template #twofaDocLink="{ content }"> <gl-link :href="config.twoFactorAuthHelpLink" target="_blank">{{ content }}</gl-link> </template> - <template #personalAccessTokensDocLink="{content}"> + <template #personalAccessTokensDocLink="{ content }"> <gl-link :href="config.personalAccessTokensHelpLink" target="_blank">{{ content }}</gl-link> @@ -81,7 +81,7 @@ export default { <p class="gl-mb-4"> {{ $options.i18n.addImageText }} </p> - <gl-form-input-group class="gl-mb-4 "> + <gl-form-input-group class="gl-mb-4"> <gl-form-input :value="dockerBuildCommand" readonly diff --git a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue b/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue index c2bd01701df..f01e3c9d24a 100644 --- a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue +++ b/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue @@ -13,6 +13,7 @@ import { } from '../../constants/index'; export default { + name: 'ListHeader', components: { TitleArea, MetadataItem, @@ -43,6 +44,11 @@ export default { required: false, default: false, }, + metadataLoading: { + type: Boolean, + required: false, + default: false, + }, }, loader: { repeat: 10, @@ -92,7 +98,11 @@ export default { </script> <template> - <title-area :title="$options.i18n.CONTAINER_REGISTRY_TITLE" :info-messages="infoMessages"> + <title-area + :title="$options.i18n.CONTAINER_REGISTRY_TITLE" + :info-messages="infoMessages" + :metadata-loading="metadataLoading" + > <template #right-actions> <slot name="commands"></slot> </template> diff --git a/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue b/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue index 1cedcc41b2b..e77eda31596 100644 --- a/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue +++ b/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue @@ -1,66 +1,51 @@ <script> -/* eslint-disable vue/no-v-html */ -// We are forced to use `v-html` untill this gitlab-ui issue is resolved: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1079 -// then we can re-write this to use gl-breadcrumb -import { initial, first, last } from 'lodash'; -import { sanitize } from '~/lib/dompurify'; +// We are using gl-breadcrumb only at the last child of the handwritten breadcrumb +// until this gitlab-ui issue is resolved: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1079 +// +// See the CSS workaround in app/assets/stylesheets/pages/registry.scss when this file is changed. +import { GlBreadcrumb, GlIcon } from '@gitlab/ui'; export default { - props: { - crumbs: { - type: Array, - required: true, - }, + components: { + GlBreadcrumb, + GlIcon, }, computed: { - parsedCrumbs() { - return this.crumbs.map(c => ({ ...c, innerHTML: sanitize(c.innerHTML) })); - }, rootRoute() { - return this.$router.options.routes.find(r => r.meta.root); + return this.$router.options.routes.find((r) => r.meta.root); + }, + detailsRoute() { + return this.$router.options.routes.find((r) => r.name === 'details'); }, isRootRoute() { return this.$route.name === this.rootRoute.name; }, - rootCrumbs() { - return initial(this.parsedCrumbs); - }, - divider() { - const { classList, tagName, innerHTML } = first(this.crumbs).querySelector('svg'); - return { classList: [...classList], tagName, innerHTML: sanitize(innerHTML) }; + isLoaded() { + return this.isRootRoute || this.$store?.state.imageDetails?.name; }, - lastCrumb() { - const { children } = last(this.crumbs); - const { tagName, className } = first(children); - return { - tagName, - className, - text: this.$route.meta.nameGenerator(), - path: { to: this.$route.name }, - }; + allCrumbs() { + const crumbs = [ + { + text: this.rootRoute.meta.nameGenerator(), + to: this.rootRoute.path, + }, + ]; + if (!this.isRootRoute) { + crumbs.push({ + text: this.detailsRoute.meta.nameGenerator(), + href: this.detailsRoute.meta.path, + }); + } + return crumbs; }, }, }; </script> <template> - <ul> - <li - v-for="(crumb, index) in rootCrumbs" - :key="index" - :class="crumb.className" - v-html="crumb.innerHTML" - ></li> - <li v-if="!isRootRoute"> - <router-link ref="rootRouteLink" :to="rootRoute.path"> - {{ rootRoute.meta.nameGenerator() }} - </router-link> - <component :is="divider.tagName" :class="divider.classList" v-html="divider.innerHTML" /> - </li> - <li> - <component :is="lastCrumb.tagName" ref="lastCrumb" :class="lastCrumb.className"> - <router-link ref="childRouteLink" :to="lastCrumb.path">{{ lastCrumb.text }}</router-link> - </component> - </li> - </ul> + <gl-breadcrumb :key="isLoaded" :items="allCrumbs"> + <template #separator> + <gl-icon name="angle-right" :size="8" /> + </template> + </gl-breadcrumb> </template> diff --git a/app/assets/javascripts/registry/explorer/constants/details.js b/app/assets/javascripts/registry/explorer/constants/details.js index 1babaaa93da..b5627352857 100644 --- a/app/assets/javascripts/registry/explorer/constants/details.js +++ b/app/assets/javascripts/registry/explorer/constants/details.js @@ -60,6 +60,22 @@ export const UPDATED_AT = s__('ContainerRegistry|Last updated %{time}'); export const NOT_AVAILABLE_TEXT = __('N/A'); export const NOT_AVAILABLE_SIZE = __('0 bytes'); + +export const CLEANUP_UNSCHEDULED_TEXT = s__('ContainerRegistry|Cleanup will run %{time}'); +export const CLEANUP_SCHEDULED_TEXT = s__('ContainerRegistry|Cleanup pending'); +export const CLEANUP_ONGOING_TEXT = s__('ContainerRegistry|Cleanup in progress'); +export const CLEANUP_UNFINISHED_TEXT = s__('ContainerRegistry|Cleanup incomplete'); +export const CLEANUP_DISABLED_TEXT = s__('ContainerRegistry|Cleanup disabled'); + +export const CLEANUP_SCHEDULED_TOOLTIP = s__('ContainerRegistry|Cleanup will run soon'); +export const CLEANUP_ONGOING_TOOLTIP = s__('ContainerRegistry|Cleanup is currently removing tags'); +export const CLEANUP_UNFINISHED_TOOLTIP = s__( + 'ContainerRegistry|Cleanup ran but some tags were not removed', +); +export const CLEANUP_DISABLED_TOOLTIP = s__( + 'ContainerRegistry|Cleanup is disabled for this project', +); + // Parameters export const DEFAULT_PAGE = 1; @@ -76,3 +92,8 @@ export const ALERT_MESSAGES = { [ALERT_SUCCESS_TAGS]: DELETE_TAGS_SUCCESS_MESSAGE, [ALERT_DANGER_TAGS]: DELETE_TAGS_ERROR_MESSAGE, }; + +export const UNFINISHED_STATUS = 'UNFINISHED'; +export const UNSCHEDULED_STATUS = 'UNSCHEDULED'; +export const SCHEDULED_STATUS = 'SCHEDULED'; +export const ONGOING_STATUS = 'ONGOING'; diff --git a/app/assets/javascripts/registry/explorer/graphql/fragments/container_repository.fragment.graphql b/app/assets/javascripts/registry/explorer/graphql/fragments/container_repository.fragment.graphql deleted file mode 100644 index 9a3579ee8e0..00000000000 --- a/app/assets/javascripts/registry/explorer/graphql/fragments/container_repository.fragment.graphql +++ /dev/null @@ -1,11 +0,0 @@ -fragment ContainerRepositoryFields on ContainerRepository { - id - name - path - status - location - canDelete - createdAt - tagsCount - expirationPolicyStartedAt -} diff --git a/app/assets/javascripts/registry/explorer/graphql/index.js b/app/assets/javascripts/registry/explorer/graphql/index.js index 16152eb81f6..d934bcc7419 100644 --- a/app/assets/javascripts/registry/explorer/graphql/index.js +++ b/app/assets/javascripts/registry/explorer/graphql/index.js @@ -8,6 +8,7 @@ export const apolloProvider = new VueApollo({ defaultClient: createDefaultClient( {}, { + batchMax: 1, assumeImmutableResults: true, }, ), diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql new file mode 100644 index 00000000000..8b6d778c655 --- /dev/null +++ b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql @@ -0,0 +1,26 @@ +query getContainerRepositoriesDetails( + $fullPath: ID! + $name: String + $first: Int + $last: Int + $after: String + $before: String + $isGroupPage: Boolean! +) { + project(fullPath: $fullPath) @skip(if: $isGroupPage) { + containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) { + nodes { + id + tagsCount + } + } + } + group(fullPath: $fullPath) @include(if: $isGroupPage) { + containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) { + nodes { + id + tagsCount + } + } + } +} diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql index b40200e020b..3fd019467ac 100644 --- a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql +++ b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql @@ -18,6 +18,7 @@ query getContainerRepositoryDetails( updatedAt tagsCount expirationPolicyStartedAt + expirationPolicyCleanupStatus tags(after: $after, before: $before, first: $first, last: $last) { nodes { digest @@ -36,6 +37,10 @@ query getContainerRepositoryDetails( } project { visibility + containerExpirationPolicy { + enabled + nextRunAt + } } } } diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_group_container_repositories.query.graphql b/app/assets/javascripts/registry/explorer/graphql/queries/get_group_container_repositories.query.graphql deleted file mode 100644 index 348eda97ea7..00000000000 --- a/app/assets/javascripts/registry/explorer/graphql/queries/get_group_container_repositories.query.graphql +++ /dev/null @@ -1,23 +0,0 @@ -#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" -#import "../fragments/container_repository.fragment.graphql" - -query getGroupContainerRepositories( - $fullPath: ID! - $name: String - $first: Int - $last: Int - $after: String - $before: String -) { - group(fullPath: $fullPath) { - containerRepositoriesCount - containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) { - nodes { - ...ContainerRepositoryFields - } - pageInfo { - ...PageInfo - } - } - } -} diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_project_container_repositories.query.graphql b/app/assets/javascripts/registry/explorer/graphql/queries/get_project_container_repositories.query.graphql deleted file mode 100644 index 338e27745f7..00000000000 --- a/app/assets/javascripts/registry/explorer/graphql/queries/get_project_container_repositories.query.graphql +++ /dev/null @@ -1,23 +0,0 @@ -#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" -#import "../fragments/container_repository.fragment.graphql" - -query getProjectContainerRepositories( - $fullPath: ID! - $name: String - $first: Int - $last: Int - $after: String - $before: String -) { - project(fullPath: $fullPath) { - containerRepositoriesCount - containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) { - nodes { - ...ContainerRepositoryFields - } - pageInfo { - ...PageInfo - } - } - } -} diff --git a/app/assets/javascripts/registry/explorer/index.js b/app/assets/javascripts/registry/explorer/index.js index d887b6a1b15..a3890ab5c42 100644 --- a/app/assets/javascripts/registry/explorer/index.js +++ b/app/assets/javascripts/registry/explorer/index.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import { GlToast } from '@gitlab/ui'; import Translate from '~/vue_shared/translate'; import { parseBoolean } from '~/lib/utils/common_utils'; +import PerformancePlugin from '~/performance/vue_performance_plugin'; import RegistryExplorer from './pages/index.vue'; import RegistryBreadcrumb from './components/registry_breadcrumb.vue'; import createRouter from './router'; @@ -10,6 +11,17 @@ import { apolloProvider } from './graphql/index'; Vue.use(Translate); Vue.use(GlToast); +Vue.use(PerformancePlugin, { + components: [ + 'RegistryListPage', + 'ListHeader', + 'ImageListRow', + 'RegistryDetailsPage', + 'DetailsHeader', + 'TagsList', + ], +}); + export default () => { const el = document.getElementById('js-container-registry'); @@ -59,16 +71,28 @@ export default () => { }); const attachBreadcrumb = () => { - const breadCrumbEl = document.querySelector('nav .js-breadcrumbs-list'); - const crumbs = [...document.querySelectorAll('.js-breadcrumbs-list li')]; + const breadCrumbEls = document.querySelectorAll('nav .js-breadcrumbs-list li'); + const breadCrumbEl = breadCrumbEls[breadCrumbEls.length - 1]; + const crumbs = [breadCrumbEl.querySelector('h2')]; + const nestedBreadcrumbEl = document.createElement('div'); + breadCrumbEl.replaceChild(nestedBreadcrumbEl, breadCrumbEl.querySelector('h2')); return new Vue({ - el: breadCrumbEl, + el: nestedBreadcrumbEl, router, apolloProvider, components: { RegistryBreadcrumb, }, render(createElement) { + // FIXME(@tnir): this is a workaround until the MR gets merged: + // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48115 + const parentEl = breadCrumbEl.parentElement.parentElement; + if (parentEl) { + parentEl.classList.remove('breadcrumbs-container'); + parentEl.classList.add('gl-display-flex'); + parentEl.classList.add('w-100'); + } + // End of FIXME(@tnir) return createElement('registry-breadcrumb', { class: breadCrumbEl.className, props: { diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue index 540f02d58d4..0894fd6fcfa 100644 --- a/app/assets/javascripts/registry/explorer/pages/details.vue +++ b/app/assets/javascripts/registry/explorer/pages/details.vue @@ -22,9 +22,11 @@ import { ALERT_DANGER_TAGS, GRAPHQL_PAGE_SIZE, FETCH_IMAGES_LIST_ERROR_MESSAGE, + UNFINISHED_STATUS, } from '../constants/index'; export default { + name: 'RegistryDetailsPage', components: { DeleteAlert, PartialCleanupAlert, @@ -35,11 +37,11 @@ export default { TagsLoader, EmptyTagsState, }, - inject: ['breadCrumbState', 'config'], directives: { GlResizeObserver: GlResizeObserverDirective, }, mixins: [Tracking.mixin()], + inject: ['breadCrumbState', 'config'], apollo: { image: { query: getContainerRepositoryDetailsQuery, @@ -83,7 +85,10 @@ export default { return this.image?.tags?.nodes || []; }, showPartialCleanupWarning() { - return this.image?.expirationPolicyStartedAt && !this.dismissPartialCleanupWarning; + return ( + this.image?.expirationPolicyCleanupStatus === UNFINISHED_STATUS && + !this.dismissPartialCleanupWarning + ); }, tracking() { return { @@ -97,7 +102,7 @@ export default { }, methods: { deleteTags(toBeDeleted) { - this.itemsToBeDeleted = this.tags.filter(tag => toBeDeleted[tag.name]); + this.itemsToBeDeleted = this.tags.filter((tag) => toBeDeleted[tag.name]); this.track('click_button'); this.$refs.deleteModal.show(); }, @@ -111,7 +116,7 @@ export default { mutation: deleteContainerRepositoryTagsMutation, variables: { id: this.queryVariables.id, - tagNames: itemsToBeDeleted.map(i => i.name), + tagNames: itemsToBeDeleted.map((i) => i.name), }, awaitRefetchQueries: true, refetchQueries: [ @@ -183,7 +188,7 @@ export default { @dismiss="dismissPartialCleanupWarning = true" /> - <details-header :image="image" /> + <details-header :image="image" :metadata-loading="isLoading" /> <tags-loader v-if="isLoading" /> <template v-else> diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/registry/explorer/pages/list.vue index 3192ba82db8..336a997d629 100644 --- a/app/assets/javascripts/registry/explorer/pages/list.vue +++ b/app/assets/javascripts/registry/explorer/pages/list.vue @@ -9,17 +9,13 @@ import { GlSkeletonLoader, GlSearchBoxByClick, } from '@gitlab/ui'; +import { get } from 'lodash'; +import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql'; import Tracking from '~/tracking'; import createFlash from '~/flash'; - -import ProjectEmptyState from '../components/list_page/project_empty_state.vue'; -import GroupEmptyState from '../components/list_page/group_empty_state.vue'; import RegistryHeader from '../components/list_page/registry_header.vue'; -import ImageList from '../components/list_page/image_list.vue'; -import CliCommands from '../components/list_page/cli_commands.vue'; -import getProjectContainerRepositoriesQuery from '../graphql/queries/get_project_container_repositories.query.graphql'; -import getGroupContainerRepositoriesQuery from '../graphql/queries/get_group_container_repositories.query.graphql'; +import getContainerRepositoriesDetails from '../graphql/queries/get_container_repositories_details.query.graphql'; import deleteContainerRepositoryMutation from '../graphql/mutations/delete_container_repository.mutation.graphql'; import { @@ -38,12 +34,25 @@ import { } from '../constants/index'; export default { - name: 'RegistryListApp', + name: 'RegistryListPage', components: { GlEmptyState, - ProjectEmptyState, - GroupEmptyState, - ImageList, + ProjectEmptyState: () => + import( + /* webpackChunkName: 'container_registry_components' */ '../components/list_page/project_empty_state.vue' + ), + GroupEmptyState: () => + import( + /* webpackChunkName: 'container_registry_components' */ '../components/list_page/group_empty_state.vue' + ), + ImageList: () => + import( + /* webpackChunkName: 'container_registry_components' */ '../components/list_page/image_list.vue' + ), + CliCommands: () => + import( + /* webpackChunkName: 'container_registry_components' */ '../components/list_page/cli_commands.vue' + ), GlModal, GlSprintf, GlLink, @@ -51,13 +60,12 @@ export default { GlSkeletonLoader, GlSearchBoxByClick, RegistryHeader, - CliCommands, }, - inject: ['config'], directives: { GlTooltip: GlTooltipDirective, }, mixins: [Tracking.mixin()], + inject: ['config'], loader: { repeat: 10, width: 1000, @@ -74,10 +82,8 @@ export default { EMPTY_RESULT_MESSAGE, }, apollo: { - images: { - query() { - return this.graphQlQuery; - }, + baseImages: { + query: getContainerRepositoriesQuery, variables() { return this.queryVariables; }, @@ -92,10 +98,26 @@ export default { createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE }); }, }, + additionalDetails: { + skip() { + return !this.fetchAdditionalDetails; + }, + query: getContainerRepositoriesDetails, + variables() { + return this.queryVariables; + }, + update(data) { + return data[this.graphqlResource]?.containerRepositories.nodes; + }, + error() { + createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE }); + }, + }, }, data() { return { - images: [], + baseImages: [], + additionalDetails: [], pageInfo: {}, containerRepositoriesCount: 0, itemToDelete: {}, @@ -103,21 +125,24 @@ export default { searchValue: null, name: null, mutationLoading: false, + fetchAdditionalDetails: false, }; }, computed: { + images() { + return this.baseImages.map((image, index) => ({ + ...image, + ...get(this.additionalDetails, index, {}), + })); + }, graphqlResource() { return this.config.isGroupPage ? 'group' : 'project'; }, - graphQlQuery() { - return this.config.isGroupPage - ? getGroupContainerRepositoriesQuery - : getProjectContainerRepositoriesQuery; - }, queryVariables() { return { name: this.name, fullPath: this.config.isGroupPage ? this.config.groupPath : this.config.projectPath, + isGroupPage: this.config.isGroupPage, first: GRAPHQL_PAGE_SIZE, }; }, @@ -127,7 +152,7 @@ export default { }; }, isLoading() { - return this.$apollo.queries.images.loading || this.mutationLoading; + return this.$apollo.queries.baseImages.loading || this.mutationLoading; }, showCommands() { return Boolean(!this.isLoading && !this.config?.isGroupPage && this.images?.length); @@ -141,6 +166,13 @@ export default { : DELETE_IMAGE_ERROR_MESSAGE; }, }, + mounted() { + // If the two graphql calls - which are not batched - resolve togheter we will have a race + // condition when apollo sets the cache, with this we give the 'base' call an headstart + setTimeout(() => { + this.fetchAdditionalDetails = true; + }, 200); + }, methods: { deleteImage(item) { this.track('click_button'); @@ -175,30 +207,46 @@ export default { this.deleteAlertType = null; this.itemToDelete = {}; }, - fetchNextPage() { + updateQuery(_, { fetchMoreResult }) { + return fetchMoreResult; + }, + async fetchNextPage() { if (this.pageInfo?.hasNextPage) { - this.$apollo.queries.images.fetchMore({ - variables: { - after: this.pageInfo?.endCursor, - first: GRAPHQL_PAGE_SIZE, - }, - updateQuery(previousResult, { fetchMoreResult }) { - return fetchMoreResult; - }, + const variables = { + after: this.pageInfo?.endCursor, + first: GRAPHQL_PAGE_SIZE, + }; + + this.$apollo.queries.baseImages.fetchMore({ + variables, + updateQuery: this.updateQuery, + }); + + await this.$nextTick(); + + this.$apollo.queries.additionalDetails.fetchMore({ + variables, + updateQuery: this.updateQuery, }); } }, - fetchPreviousPage() { + async fetchPreviousPage() { if (this.pageInfo?.hasPreviousPage) { - this.$apollo.queries.images.fetchMore({ - variables: { - first: null, - before: this.pageInfo?.startCursor, - last: GRAPHQL_PAGE_SIZE, - }, - updateQuery(previousResult, { fetchMoreResult }) { - return fetchMoreResult; - }, + const variables = { + first: null, + before: this.pageInfo?.startCursor, + last: GRAPHQL_PAGE_SIZE, + }; + this.$apollo.queries.baseImages.fetchMore({ + variables, + updateQuery: this.updateQuery, + }); + + await this.$nextTick(); + + this.$apollo.queries.additionalDetails.fetchMore({ + variables, + updateQuery: this.updateQuery, }); } }, @@ -230,7 +278,7 @@ export default { <template #description> <p> <gl-sprintf :message="$options.i18n.CONNECTION_ERROR_MESSAGE"> - <template #docLink="{content}"> + <template #docLink="{ content }"> <gl-link :href="`${config.helpPagePath}#docker-connection-error`" target="_blank"> {{ content }} </gl-link> @@ -242,6 +290,7 @@ export default { <template v-else> <registry-header + :metadata-loading="isLoading" :images-count="containerRepositoriesCount" :expiration-policy="config.expirationPolicy" :help-page-path="config.helpPagePath" @@ -285,6 +334,7 @@ export default { <image-list v-if="images.length" :images="images" + :metadata-loading="$apollo.queries.additionalDetails.loading" :page-info="pageInfo" @delete="deleteImage" @prev-page="fetchPreviousPage" diff --git a/app/assets/javascripts/registry/settings/components/expiration_input.vue b/app/assets/javascripts/registry/settings/components/expiration_input.vue index 2dbd9d26f60..42b7c7918a5 100644 --- a/app/assets/javascripts/registry/settings/components/expiration_input.vue +++ b/app/assets/javascripts/registry/settings/components/expiration_input.vue @@ -83,7 +83,7 @@ export default { <template #label> <span data-testid="label"> <gl-sprintf :message="label"> - <template #italic="{content}"> + <template #italic="{ content }"> <i>{{ content }}</i> </template> </gl-sprintf> @@ -100,7 +100,7 @@ export default { <template #description> <span data-testid="description" class="gl-text-gray-400"> <gl-sprintf :message="description"> - <template #link="{content}"> + <template #link="{ content }"> <gl-link :href="tagsRegexHelpPagePath" target="_blank">{{ content }}</gl-link> </template> </gl-sprintf> diff --git a/app/assets/javascripts/registry/settings/components/expiration_toggle.vue b/app/assets/javascripts/registry/settings/components/expiration_toggle.vue index 7f045244926..0ffd8216ab1 100644 --- a/app/assets/javascripts/registry/settings/components/expiration_toggle.vue +++ b/app/assets/javascripts/registry/settings/components/expiration_toggle.vue @@ -42,7 +42,7 @@ export default { <gl-toggle id="expiration-policy-toggle" v-model="enabled" :disabled="disabled" /> <span class="gl-ml-5 gl-line-height-24" data-testid="description"> <gl-sprintf :message="toggleText"> - <template #strong="{content}"> + <template #strong="{ content }"> <strong>{{ content }}</strong> </template> </gl-sprintf> diff --git a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue index 35c7a8be4ea..66eb681784e 100644 --- a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue +++ b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue @@ -33,7 +33,7 @@ export default { projectPath: this.projectPath, }; }, - update: data => data.project?.containerExpirationPolicy, + update: (data) => data.project?.containerExpirationPolicy, result({ data }) { this.workingCopy = { ...get(data, 'project.containerExpirationPolicy', {}) }; }, diff --git a/app/assets/javascripts/registry/settings/components/settings_form.vue b/app/assets/javascripts/registry/settings/components/settings_form.vue index 1f374c7b60e..7043cea49ba 100644 --- a/app/assets/javascripts/registry/settings/components/settings_form.vue +++ b/app/assets/javascripts/registry/settings/components/settings_form.vue @@ -96,7 +96,7 @@ export default { return this.isLoading || this.mutationLoading; }, fieldsAreValid() { - return Object.values(this.localErrors).every(error => error); + return Object.values(this.localErrors).every((error) => error); }, isSubmitButtonDisabled() { return !this.fieldsAreValid || this.showLoadingIcon; @@ -121,7 +121,7 @@ export default { }, methods: { findDefaultOption(option) { - return this.value[option] || this.$options.formOptions[option].find(f => f.default)?.key; + return this.value[option] || this.$options.formOptions[option].find((f) => f.default)?.key; }, reset() { this.track('reset_form'); @@ -131,7 +131,7 @@ export default { }, setApiErrors(response) { this.apiErrors = response.graphQLErrors.reduce((acc, curr) => { - curr.extensions.problems.forEach(item => { + curr.extensions.problems.forEach((item) => { acc[item.path[0]] = item.message; }); return acc; @@ -163,7 +163,7 @@ export default { this.$toast.show(UPDATE_SETTINGS_SUCCESS_MESSAGE, { type: 'success' }); } }) - .catch(error => { + .catch((error) => { this.setApiErrors(error); this.$toast.show(UPDATE_SETTINGS_ERROR_MESSAGE, { type: 'error' }); }) @@ -214,10 +214,10 @@ export default { <div> <p> <gl-sprintf :message="$options.i18n.KEEP_INFO_TEXT"> - <template #strong="{content}"> + <template #strong="{ content }"> <strong>{{ content }}</strong> </template> - <template #secondStrong="{content}"> + <template #secondStrong="{ content }"> <strong>{{ content }}</strong> </template> </gl-sprintf> @@ -253,10 +253,10 @@ export default { <div> <p> <gl-sprintf :message="$options.i18n.REMOVE_INFO_TEXT"> - <template #strong="{content}"> + <template #strong="{ content }"> <strong>{{ content }}</strong> </template> - <template #secondStrong="{content}"> + <template #secondStrong="{ content }"> <strong>{{ content }}</strong> </template> </gl-sprintf> diff --git a/app/assets/javascripts/registry/settings/constants.js b/app/assets/javascripts/registry/settings/constants.js index 21c54299632..165c4aae3cb 100644 --- a/app/assets/javascripts/registry/settings/constants.js +++ b/app/assets/javascripts/registry/settings/constants.js @@ -23,7 +23,7 @@ export const KEEP_INFO_TEXT = s__( export const KEEP_N_LABEL = s__('ContainerRegistry|Keep the most recent:'); export const NAME_REGEX_KEEP_LABEL = s__('ContainerRegistry|Keep tags matching:'); export const NAME_REGEX_KEEP_DESCRIPTION = s__( - 'ContainerRegistry|Tags with names that match this regex pattern are kept. %{linkStart}More information%{linkEnd}', + 'ContainerRegistry|Tags with names that match this regex pattern are kept. %{linkStart}View regex examples.%{linkEnd}', ); export const REMOVE_HEADER_TEXT = s__('ContainerRegistry|Remove these tags'); @@ -34,7 +34,7 @@ export const EXPIRATION_SCHEDULE_LABEL = s__('ContainerRegistry|Remove tags olde export const NAME_REGEX_LABEL = s__('ContainerRegistry|Remove tags matching:'); export const NAME_REGEX_PLACEHOLDER = '.*'; export const NAME_REGEX_DESCRIPTION = s__( - 'ContainerRegistry|Tags with names that match this regex pattern are removed. %{linkStart}More information%{linkEnd}', + 'ContainerRegistry|Tags with names that match this regex pattern are removed. %{linkStart}View regex examples.%{linkEnd}', ); export const ENABLED_TOGGLE_DESCRIPTION = s__( diff --git a/app/assets/javascripts/registry/settings/graphql/utils/cache_update.js b/app/assets/javascripts/registry/settings/graphql/utils/cache_update.js index 05b4125a2fc..6becaa38c7e 100644 --- a/app/assets/javascripts/registry/settings/graphql/utils/cache_update.js +++ b/app/assets/javascripts/registry/settings/graphql/utils/cache_update.js @@ -1,14 +1,14 @@ import { produce } from 'immer'; import expirationPolicyQuery from '../queries/get_expiration_policy.query.graphql'; -export const updateContainerExpirationPolicy = projectPath => (client, { data: updatedData }) => { +export const updateContainerExpirationPolicy = (projectPath) => (client, { data: updatedData }) => { const queryAndParams = { query: expirationPolicyQuery, variables: { projectPath }, }; const sourceData = client.readQuery(queryAndParams); - const data = produce(sourceData, draftState => { + const data = produce(sourceData, (draftState) => { // eslint-disable-next-line no-param-reassign draftState.project.containerExpirationPolicy = { ...updatedData.updateContainerExpirationPolicy.containerExpirationPolicy, diff --git a/app/assets/javascripts/registry/settings/utils.js b/app/assets/javascripts/registry/settings/utils.js index 51b4fb6bdb8..4a2d7c7d466 100644 --- a/app/assets/javascripts/registry/settings/utils.js +++ b/app/assets/javascripts/registry/settings/utils.js @@ -1,18 +1,18 @@ import { n__ } from '~/locale'; import { KEEP_N_OPTIONS, CADENCE_OPTIONS, OLDER_THAN_OPTIONS } from './constants'; -export const findDefaultOption = options => { - const item = options.find(o => o.default); +export const findDefaultOption = (options) => { + const item = options.find((o) => o.default); return item ? item.key : null; }; -export const olderThanTranslationGenerator = variable => n__('%d day', '%d days', variable); +export const olderThanTranslationGenerator = (variable) => n__('%d day', '%d days', variable); -export const keepNTranslationGenerator = variable => +export const keepNTranslationGenerator = (variable) => n__('%d tag per image name', '%d tags per image name', variable); export const optionLabelGenerator = (collection, translationFn) => - collection.map(option => ({ + collection.map((option) => ({ ...option, label: translationFn(option.variable), })); diff --git a/app/assets/javascripts/related_issues/components/related_issuable_input.vue b/app/assets/javascripts/related_issues/components/related_issuable_input.vue index b05a873e939..a124b055e19 100644 --- a/app/assets/javascripts/related_issues/components/related_issuable_input.vue +++ b/app/assets/javascripts/related_issues/components/related_issuable_input.vue @@ -118,7 +118,7 @@ export default { let position = 0; const untouchedRawRefs = rawRefs - .filter(ref => { + .filter((ref) => { let isTouched = false; if (caretPos >= position && caretPos <= position + ref.length) { @@ -130,7 +130,7 @@ export default { return !isTouched; }) - .filter(ref => ref.trim().length > 0); + .filter((ref) => ref.trim().length > 0); this.$emit('addIssuableFormInput', { newValue: value, @@ -208,7 +208,7 @@ export default { :path-id-separator="pathIdSeparator" event-namespace="pendingIssuable" @pendingIssuableRemoveRequest=" - params => { + (params) => { $emit('pendingIssuableRemoveRequest', params); } " diff --git a/app/assets/javascripts/related_issues/components/related_issues_block.vue b/app/assets/javascripts/related_issues/components/related_issues_block.vue index c913745a8e1..2591e3e7f48 100644 --- a/app/assets/javascripts/related_issues/components/related_issues_block.vue +++ b/app/assets/javascripts/related_issues/components/related_issues_block.vue @@ -90,11 +90,11 @@ export default { categorisedIssues() { if (this.showCategorizedIssues) { return Object.values(linkedIssueTypesMap) - .map(linkType => ({ + .map((linkType) => ({ linkType, - issues: this.relatedIssues.filter(issue => issue.linkType === linkType), + issues: this.relatedIssues.filter((issue) => issue.linkType === linkType), })) - .filter(obj => obj.issues.length > 0); + .filter((obj) => obj.issues.length > 0); } return [{ issues: this.relatedIssues }]; diff --git a/app/assets/javascripts/related_issues/components/related_issues_root.vue b/app/assets/javascripts/related_issues/components/related_issues_root.vue index 73ea13ddc40..a81edcf141c 100644 --- a/app/assets/javascripts/related_issues/components/related_issues_root.vue +++ b/app/assets/javascripts/related_issues/components/related_issues_root.vue @@ -110,7 +110,7 @@ export default { }, methods: { findRelatedIssueById(id) { - return this.state.relatedIssues.find(issue => issue.id === id); + return this.state.relatedIssues.find((issue) => issue.id === id); }, onRelatedIssueRemoveRequest(idToRemove) { const issueToRemove = this.findRelatedIssueById(idToRemove); @@ -120,7 +120,7 @@ export default { .then(({ data }) => { this.store.setRelatedIssues(data.issuables); }) - .catch(res => { + .catch((res) => { if (res && res.status !== 404) { Flash(relatedIssuesRemoveErrorMap[this.issuableType]); } @@ -219,7 +219,7 @@ export default { this.processAllReferences(newValue); }, processAllReferences(value = '') { - const rawReferences = value.split(/\s+/).filter(reference => reference.trim().length > 0); + const rawReferences = value.split(/\s+/).filter((reference) => reference.trim().length > 0); this.store.addPendingReferences(rawReferences); this.inputValue = ''; diff --git a/app/assets/javascripts/related_issues/index.js b/app/assets/javascripts/related_issues/index.js index 2e8626890cb..0ee99df1455 100644 --- a/app/assets/javascripts/related_issues/index.js +++ b/app/assets/javascripts/related_issues/index.js @@ -11,7 +11,7 @@ export default function initRelatedIssues() { components: { relatedIssuesRoot: RelatedIssuesRoot, }, - render: createElement => + render: (createElement) => createElement('related-issues-root', { props: { endpoint: relatedIssuesRootElement.dataset.endpoint, diff --git a/app/assets/javascripts/related_issues/stores/related_issues_store.js b/app/assets/javascripts/related_issues/stores/related_issues_store.js index 14d71628cad..0f0ab150c28 100644 --- a/app/assets/javascripts/related_issues/stores/related_issues_store.js +++ b/app/assets/javascripts/related_issues/stores/related_issues_store.js @@ -19,7 +19,7 @@ class RelatedIssuesStore { } removeRelatedIssue(issue) { - this.state.relatedIssues = this.state.relatedIssues.filter(x => x.id !== issue.id); + this.state.relatedIssues = this.state.relatedIssues.filter((x) => x.id !== issue.id); } updateIssueOrder(oldIndex, newIndex) { diff --git a/app/assets/javascripts/related_merge_requests/index.js b/app/assets/javascripts/related_merge_requests/index.js index 092ff1df00f..ce33cf7df1d 100644 --- a/app/assets/javascripts/related_merge_requests/index.js +++ b/app/assets/javascripts/related_merge_requests/index.js @@ -15,7 +15,7 @@ export default function initRelatedMergeRequests() { RelatedMergeRequests, }, store: createStore(), - render: createElement => + render: (createElement) => createElement('related-merge-requests', { props: { endpoint, projectNamespace, projectPath }, }), diff --git a/app/assets/javascripts/related_merge_requests/store/actions.js b/app/assets/javascripts/related_merge_requests/store/actions.js index 65f77f2fe19..7baab165820 100644 --- a/app/assets/javascripts/related_merge_requests/store/actions.js +++ b/app/assets/javascripts/related_merge_requests/store/actions.js @@ -21,7 +21,7 @@ export const fetchMergeRequests = ({ state, dispatch }) => { return axios .get(`${state.apiEndpoint}?per_page=${REQUEST_PAGE_COUNT}`) - .then(res => { + .then((res) => { const { headers, data } = res; const total = Number(normalizeHeaders(headers)['X-TOTAL']) || 0; diff --git a/app/assets/javascripts/releases/components/release_block_assets.vue b/app/assets/javascripts/releases/components/release_block_assets.vue index eb83d8657c0..36929f559b5 100644 --- a/app/assets/javascripts/releases/components/release_block_assets.vue +++ b/app/assets/javascripts/releases/components/release_block_assets.vue @@ -47,7 +47,7 @@ export default { sections() { return [ { - links: get(this.assets, 'sources', []).map(s => ({ + links: get(this.assets, 'sources', []).map((s) => ({ url: s.url, name: sprintf(__('Source code (%{fileExtension})'), { fileExtension: s.format }), })), @@ -73,7 +73,7 @@ export default { links: this.otherLinks, iconName: 'link', }, - ].filter(section => section.links.length > 0); + ].filter((section) => section.links.length > 0); }, }, methods: { @@ -81,7 +81,7 @@ export default { this.isAssetsExpanded = !this.isAssetsExpanded; }, linksForType(type) { - return this.assets.links.filter(l => l.linkType === type); + return this.assets.links.filter((l) => l.linkType === type); }, }, externalLinkTooltipText: __('This link points to external content'), diff --git a/app/assets/javascripts/releases/components/release_block_milestone_info.vue b/app/assets/javascripts/releases/components/release_block_milestone_info.vue index daa9c3480f4..cf4a6e07af7 100644 --- a/app/assets/javascripts/releases/components/release_block_milestone_info.vue +++ b/app/assets/javascripts/releases/components/release_block_milestone_info.vue @@ -64,7 +64,7 @@ export default { }, issueCounts() { return this.milestones - .map(m => m.issueStats || {}) + .map((m) => m.issueStats || {}) .reduce( (acc, current) => { acc.total += current.total || 0; @@ -79,11 +79,11 @@ export default { ); }, showMergeRequestStats() { - return this.milestones.some(m => m.mrStats); + return this.milestones.some((m) => m.mrStats); }, mergeRequestCounts() { return this.milestones - .map(m => m.mrStats || {}) + .map((m) => m.mrStats || {}) .reduce( (acc, current) => { acc.total += current.total || 0; diff --git a/app/assets/javascripts/releases/components/releases_sort.vue b/app/assets/javascripts/releases/components/releases_sort.vue index 50f6f3c19bd..c8e6e0e4996 100644 --- a/app/assets/javascripts/releases/components/releases_sort.vue +++ b/app/assets/javascripts/releases/components/releases_sort.vue @@ -11,14 +11,14 @@ export default { }, computed: { ...mapState('list', { - orderBy: state => state.sorting.orderBy, - sort: state => state.sorting.sort, + orderBy: (state) => state.sorting.orderBy, + sort: (state) => state.sorting.sort, }), sortOptions() { return SORT_OPTIONS; }, sortText() { - const option = this.sortOptions.find(s => s.orderBy === this.orderBy); + const option = this.sortOptions.find((s) => s.orderBy === this.orderBy); return option.label; }, isSortAscending() { diff --git a/app/assets/javascripts/releases/constants.js b/app/assets/javascripts/releases/constants.js index 8979aa1394d..f9653e0befa 100644 --- a/app/assets/javascripts/releases/constants.js +++ b/app/assets/javascripts/releases/constants.js @@ -13,7 +13,7 @@ export const ASSET_LINK_TYPE = Object.freeze({ export const DEFAULT_ASSET_LINK_TYPE = ASSET_LINK_TYPE.OTHER; -export const PAGE_SIZE = 20; +export const PAGE_SIZE = 10; export const ASCENDING_ODER = 'asc'; export const DESCENDING_ORDER = 'desc'; diff --git a/app/assets/javascripts/releases/mount_edit.js b/app/assets/javascripts/releases/mount_edit.js index 2f4b0e64e36..1232d55847b 100644 --- a/app/assets/javascripts/releases/mount_edit.js +++ b/app/assets/javascripts/releases/mount_edit.js @@ -18,6 +18,6 @@ export default () => { return new Vue({ el, store, - render: h => h(ReleaseEditNewApp), + render: (h) => h(ReleaseEditNewApp), }); }; diff --git a/app/assets/javascripts/releases/mount_index.js b/app/assets/javascripts/releases/mount_index.js index cd4fa5c5df5..a9538cbc9e5 100644 --- a/app/assets/javascripts/releases/mount_index.js +++ b/app/assets/javascripts/releases/mount_index.js @@ -21,6 +21,6 @@ export default () => { graphqlMilestoneStats: Boolean(gon.features?.graphqlMilestoneStats), }, }), - render: h => h(ReleaseListApp), + render: (h) => h(ReleaseListApp), }); }; diff --git a/app/assets/javascripts/releases/mount_new.js b/app/assets/javascripts/releases/mount_new.js index 5c481498ffb..d85f4cf77d5 100644 --- a/app/assets/javascripts/releases/mount_new.js +++ b/app/assets/javascripts/releases/mount_new.js @@ -18,6 +18,6 @@ export default () => { return new Vue({ el, store, - render: h => h(ReleaseEditNewApp), + render: (h) => h(ReleaseEditNewApp), }); }; diff --git a/app/assets/javascripts/releases/mount_show.js b/app/assets/javascripts/releases/mount_show.js index b09ecc9fb55..f3ed7d6c5ff 100644 --- a/app/assets/javascripts/releases/mount_show.js +++ b/app/assets/javascripts/releases/mount_show.js @@ -21,6 +21,6 @@ export default () => { return new Vue({ el, store, - render: h => h(ReleaseShowApp), + render: (h) => h(ReleaseShowApp), }); }; diff --git a/app/assets/javascripts/releases/stores/getters.js b/app/assets/javascripts/releases/stores/getters.js index 6a1da63289c..2a06f398e26 100644 --- a/app/assets/javascripts/releases/stores/getters.js +++ b/app/assets/javascripts/releases/stores/getters.js @@ -2,7 +2,7 @@ * @returns {Boolean} `true` if all the feature flags * required to enable the GraphQL endpoint are enabled */ -export const useGraphQLEndpoint = rootState => { +export const useGraphQLEndpoint = (rootState) => { return Boolean( rootState.featureFlags.graphqlReleaseData && rootState.featureFlags.graphqlReleasesPage && diff --git a/app/assets/javascripts/releases/stores/modules/detail/actions.js b/app/assets/javascripts/releases/stores/modules/detail/actions.js index e8a46f40d20..127646826a6 100644 --- a/app/assets/javascripts/releases/stores/modules/detail/actions.js +++ b/app/assets/javascripts/releases/stores/modules/detail/actions.js @@ -36,12 +36,12 @@ export const fetchRelease = ({ commit, state, rootState }) => { tagName: state.tagName, }, }) - .then(response => { + .then((response) => { const { data: release } = convertOneReleaseGraphQLResponse(response); commit(types.RECEIVE_RELEASE_SUCCESS, release); }) - .catch(error => { + .catch((error) => { commit(types.RECEIVE_RELEASE_ERROR, error); createFlash(s__('Release|Something went wrong while getting the release details')); }); @@ -52,7 +52,7 @@ export const fetchRelease = ({ commit, state, rootState }) => { .then(({ data }) => { commit(types.RECEIVE_RELEASE_SUCCESS, apiJsonToRelease(data)); }) - .catch(error => { + .catch((error) => { commit(types.RECEIVE_RELEASE_ERROR, error); createFlash(s__('Release|Something went wrong while getting the release details')); }); @@ -121,7 +121,7 @@ export const createRelease = ({ commit, dispatch, state, getters }) => { .then(({ data }) => { dispatch('receiveSaveReleaseSuccess', apiJsonToRelease(data)); }) - .catch(error => { + .catch((error) => { commit(types.RECEIVE_SAVE_RELEASE_ERROR, error); createFlash(s__('Release|Something went wrong while creating a new release')); }); @@ -163,7 +163,7 @@ export const updateRelease = ({ commit, dispatch, state, getters }) => { // Delete all links currently associated with this Release return Promise.all( - getters.releaseLinksToDelete.map(l => + getters.releaseLinksToDelete.map((l) => api.deleteReleaseLink(state.projectId, state.release.tagName, l.id), ), ); @@ -171,7 +171,7 @@ export const updateRelease = ({ commit, dispatch, state, getters }) => { .then(() => { // Create a new link for each link in the form return Promise.all( - apiJson.assets.links.map(l => + apiJson.assets.links.map((l) => api.createReleaseLink(state.projectId, state.release.tagName, l), ), ); @@ -179,7 +179,7 @@ export const updateRelease = ({ commit, dispatch, state, getters }) => { .then(() => { dispatch('receiveSaveReleaseSuccess', apiJsonToRelease(updatedRelease)); }) - .catch(error => { + .catch((error) => { commit(types.RECEIVE_SAVE_RELEASE_ERROR, error); createFlash(s__('Release|Something went wrong while saving the release details')); }) diff --git a/app/assets/javascripts/releases/stores/modules/detail/getters.js b/app/assets/javascripts/releases/stores/modules/detail/getters.js index 809ed075c16..831037c8861 100644 --- a/app/assets/javascripts/releases/stores/modules/detail/getters.js +++ b/app/assets/javascripts/releases/stores/modules/detail/getters.js @@ -5,7 +5,7 @@ import { hasContent } from '~/lib/utils/text_utility'; * @returns {Boolean} `true` if the app is editing an existing release. * `false` if the app is creating a new release. */ -export const isExistingRelease = state => { +export const isExistingRelease = (state) => { return Boolean(state.tagName); }; @@ -15,19 +15,19 @@ export const isExistingRelease = state => { * empty (or whitespace-only) values for both `url` and `name`. * Otherwise, `false`. */ -const isEmptyReleaseLink = link => !hasContent(link.url) && !hasContent(link.name); +const isEmptyReleaseLink = (link) => !hasContent(link.url) && !hasContent(link.name); /** Returns all release links that aren't empty */ -export const releaseLinksToCreate = state => { +export const releaseLinksToCreate = (state) => { if (!state.release) { return []; } - return state.release.assets.links.filter(l => !isEmptyReleaseLink(l)); + return state.release.assets.links.filter((l) => !isEmptyReleaseLink(l)); }; /** Returns all release links that should be deleted */ -export const releaseLinksToDelete = state => { +export const releaseLinksToDelete = (state) => { if (!state.originalRelease) { return []; } @@ -36,7 +36,7 @@ export const releaseLinksToDelete = state => { }; /** Returns all validation errors on the release object */ -export const validationErrors = state => { +export const validationErrors = (state) => { const errors = { assets: { links: {}, @@ -56,7 +56,7 @@ export const validationErrors = state => { // This is used for detecting duplicate URLs. const urlToLinksMap = new Map(); - state.release.assets.links.forEach(link => { + state.release.assets.links.forEach((link) => { errors.assets.links[link.id] = {}; // Only validate non-empty URLs @@ -81,7 +81,7 @@ export const validationErrors = state => { // add a validation error for each link that shares this URL const duplicates = urlToLinksMap.get(normalizedUrl); duplicates.push(link); - duplicates.forEach(duplicateLink => { + duplicates.forEach((duplicateLink) => { errors.assets.links[duplicateLink.id].isDuplicate = true; }); } else { diff --git a/app/assets/javascripts/releases/stores/modules/detail/index.js b/app/assets/javascripts/releases/stores/modules/detail/index.js index 40fdb04f2eb..e1b7e69accc 100644 --- a/app/assets/javascripts/releases/stores/modules/detail/index.js +++ b/app/assets/javascripts/releases/stores/modules/detail/index.js @@ -3,7 +3,7 @@ import * as getters from './getters'; import mutations from './mutations'; import createState from './state'; -export default initialState => ({ +export default (initialState) => ({ namespaced: true, actions, getters, diff --git a/app/assets/javascripts/releases/stores/modules/detail/mutations.js b/app/assets/javascripts/releases/stores/modules/detail/mutations.js index 58a1958c5e2..8f4bfbc9b86 100644 --- a/app/assets/javascripts/releases/stores/modules/detail/mutations.js +++ b/app/assets/javascripts/releases/stores/modules/detail/mutations.js @@ -3,7 +3,7 @@ import * as types from './mutation_types'; import { DEFAULT_ASSET_LINK_TYPE } from '../../../constants'; const findReleaseLink = (release, id) => { - return release.assets.links.find(l => l.id === id); + return release.assets.links.find((l) => l.id === id); }; export default { @@ -93,6 +93,6 @@ export default { }, [types.REMOVE_ASSET_LINK](state, linkIdToRemove) { - state.release.assets.links = state.release.assets.links.filter(l => l.id !== linkIdToRemove); + state.release.assets.links = state.release.assets.links.filter((l) => l.id !== linkIdToRemove); }, }; diff --git a/app/assets/javascripts/releases/stores/modules/list/actions.js b/app/assets/javascripts/releases/stores/modules/list/actions.js index a62f7c25464..4c4f6e19a93 100644 --- a/app/assets/javascripts/releases/stores/modules/list/actions.js +++ b/app/assets/javascripts/releases/stores/modules/list/actions.js @@ -68,7 +68,7 @@ export const fetchReleasesGraphQl = ( ...paginationParams, }, }) - .then(response => { + .then((response) => { const { data, paginationInfo: graphQlPageInfo } = convertAllReleasesGraphQLResponse(response); commit(types.RECEIVE_RELEASES_SUCCESS, { diff --git a/app/assets/javascripts/releases/stores/modules/list/index.js b/app/assets/javascripts/releases/stores/modules/list/index.js index 0f97fa83ced..244f41b6609 100644 --- a/app/assets/javascripts/releases/stores/modules/list/index.js +++ b/app/assets/javascripts/releases/stores/modules/list/index.js @@ -2,7 +2,7 @@ import createState from './state'; import * as actions from './actions'; import mutations from './mutations'; -export default initialState => ({ +export default (initialState) => ({ namespaced: true, actions, mutations, diff --git a/app/assets/javascripts/releases/util.js b/app/assets/javascripts/releases/util.js index 464f0594b8d..b24a226cf9c 100644 --- a/app/assets/javascripts/releases/util.js +++ b/app/assets/javascripts/releases/util.js @@ -17,7 +17,7 @@ export const releaseToApiJson = (release, createFrom = null) => { // Milestones may be either a list of milestone objects OR just a list // of milestone titles. The API requires only the titles be sent. - const milestones = (release.milestones || []).map(m => m.title || m); + const milestones = (release.milestones || []).map((m) => m.title || m); return convertObjectPropsToSnakeCase( { @@ -37,7 +37,7 @@ export const releaseToApiJson = (release, createFrom = null) => { * into the structure this Vue application can work with. * @param {Object} json The JSON object received from the release API */ -export const apiJsonToRelease = json => { +export const apiJsonToRelease = (json) => { const release = convertObjectPropsToCamelCase(json, { deep: true }); release.milestones = release.milestones || []; @@ -47,7 +47,7 @@ export const apiJsonToRelease = json => { export const gqClient = createGqClient({}, { fetchPolicy: fetchPolicies.NO_CACHE }); -const convertScalarProperties = graphQLRelease => +const convertScalarProperties = (graphQLRelease) => pick(graphQLRelease, [ 'name', 'tagName', @@ -57,29 +57,29 @@ const convertScalarProperties = graphQLRelease => 'upcomingRelease', ]); -const convertAssets = graphQLRelease => ({ +const convertAssets = (graphQLRelease) => ({ assets: { count: graphQLRelease.assets.count, sources: [...graphQLRelease.assets.sources.nodes], - links: graphQLRelease.assets.links.nodes.map(l => ({ + links: graphQLRelease.assets.links.nodes.map((l) => ({ ...l, linkType: l.linkType?.toLowerCase(), })), }, }); -const convertEvidences = graphQLRelease => ({ - evidences: graphQLRelease.evidences.nodes.map(e => e), +const convertEvidences = (graphQLRelease) => ({ + evidences: graphQLRelease.evidences.nodes.map((e) => e), }); -const convertLinks = graphQLRelease => ({ +const convertLinks = (graphQLRelease) => ({ _links: { ...graphQLRelease.links, self: graphQLRelease.links?.selfUrl, }, }); -const convertCommit = graphQLRelease => { +const convertCommit = (graphQLRelease) => { if (!graphQLRelease.commit) { return {}; } @@ -93,10 +93,10 @@ const convertCommit = graphQLRelease => { }; }; -const convertAuthor = graphQLRelease => ({ author: graphQLRelease.author }); +const convertAuthor = (graphQLRelease) => ({ author: graphQLRelease.author }); -const convertMilestones = graphQLRelease => ({ - milestones: graphQLRelease.milestones.nodes.map(m => ({ +const convertMilestones = (graphQLRelease) => ({ + milestones: graphQLRelease.milestones.nodes.map((m) => ({ ...m, webUrl: m.webPath, webPath: undefined, @@ -115,7 +115,7 @@ const convertMilestones = graphQLRelease => ({ * * @param graphQLRelease The release object returned from a GraphQL query */ -export const convertGraphQLRelease = graphQLRelease => ({ +export const convertGraphQLRelease = (graphQLRelease) => ({ ...convertScalarProperties(graphQLRelease), ...convertAssets(graphQLRelease), ...convertEvidences(graphQLRelease), @@ -134,7 +134,7 @@ export const convertGraphQLRelease = graphQLRelease => ({ * * @param response The response received from the GraphQL endpoint */ -export const convertAllReleasesGraphQLResponse = response => { +export const convertAllReleasesGraphQLResponse = (response) => { const releases = response.data.project.releases.nodes.map(convertGraphQLRelease); const paginationInfo = { @@ -153,7 +153,7 @@ export const convertAllReleasesGraphQLResponse = response => { * * @param response The response received from the GraphQL endpoint */ -export const convertOneReleaseGraphQLResponse = response => { +export const convertOneReleaseGraphQLResponse = (response) => { const release = convertGraphQLRelease(response.data.project.release); return { data: release }; diff --git a/app/assets/javascripts/reports/accessibility_report/store/getters.js b/app/assets/javascripts/reports/accessibility_report/store/getters.js index 312b333a771..8f8eec11c7f 100644 --- a/app/assets/javascripts/reports/accessibility_report/store/getters.js +++ b/app/assets/javascripts/reports/accessibility_report/store/getters.js @@ -1,7 +1,7 @@ import { LOADING, ERROR, SUCCESS, STATUS_FAILED } from '../../constants'; import { s__, n__ } from '~/locale'; -export const groupedSummaryText = state => { +export const groupedSummaryText = (state) => { if (state.isLoading) { return s__('Reports|Accessibility scanning results are being parsed'); } @@ -22,7 +22,7 @@ export const groupedSummaryText = state => { ); }; -export const summaryStatus = state => { +export const summaryStatus = (state) => { if (state.isLoading) { return LOADING; } @@ -34,12 +34,12 @@ export const summaryStatus = state => { return SUCCESS; }; -export const shouldRenderIssuesList = state => - Object.values(state.report).some(x => Array.isArray(x) && x.length > 0); +export const shouldRenderIssuesList = (state) => + Object.values(state.report).some((x) => Array.isArray(x) && x.length > 0); // We could just map state, but we're going to iterate in the future // to add notes and warnings to these issue lists, so I'm going to // keep these as getters -export const unresolvedIssues = state => state.report.existing_errors; -export const resolvedIssues = state => state.report.resolved_errors; -export const newIssues = state => state.report.new_errors; +export const unresolvedIssues = (state) => state.report.existing_errors; +export const resolvedIssues = (state) => state.report.resolved_errors; +export const newIssues = (state) => state.report.new_errors; diff --git a/app/assets/javascripts/reports/accessibility_report/store/index.js b/app/assets/javascripts/reports/accessibility_report/store/index.js index 9fa2c589324..5bfcd69edec 100644 --- a/app/assets/javascripts/reports/accessibility_report/store/index.js +++ b/app/assets/javascripts/reports/accessibility_report/store/index.js @@ -7,11 +7,11 @@ import state from './state'; Vue.use(Vuex); -export const getStoreConfig = initialState => ({ +export const getStoreConfig = (initialState) => ({ actions, getters, mutations, state: state(initialState), }); -export default initialState => new Vuex.Store(getStoreConfig(initialState)); +export default (initialState) => new Vuex.Store(getStoreConfig(initialState)); diff --git a/app/assets/javascripts/reports/codequality_report/store/actions.js b/app/assets/javascripts/reports/codequality_report/store/actions.js index bf84d27b5ea..e5fb5caca2e 100644 --- a/app/assets/javascripts/reports/codequality_report/store/actions.js +++ b/app/assets/javascripts/reports/codequality_report/store/actions.js @@ -11,13 +11,13 @@ export const fetchReports = ({ state, dispatch, commit }) => { return dispatch('receiveReportsError'); } return Promise.all([axios.get(state.headPath), axios.get(state.basePath)]) - .then(results => + .then((results) => doCodeClimateComparison( parseCodeclimateMetrics(results[0].data, state.headBlobPath), parseCodeclimateMetrics(results[1].data, state.baseBlobPath), ), ) - .then(data => dispatch('receiveReportsSuccess', data)) + .then((data) => dispatch('receiveReportsSuccess', data)) .catch(() => dispatch('receiveReportsError')); }; diff --git a/app/assets/javascripts/reports/codequality_report/store/getters.js b/app/assets/javascripts/reports/codequality_report/store/getters.js index d7c31bcf459..e017bab976c 100644 --- a/app/assets/javascripts/reports/codequality_report/store/getters.js +++ b/app/assets/javascripts/reports/codequality_report/store/getters.js @@ -2,10 +2,10 @@ import { LOADING, ERROR, SUCCESS } from '../../constants'; import { sprintf, __, s__, n__ } from '~/locale'; import { spriteIcon } from '~/lib/utils/common_utils'; -export const hasCodequalityIssues = state => +export const hasCodequalityIssues = (state) => Boolean(state.newIssues?.length || state.resolvedIssues?.length); -export const codequalityStatus = state => { +export const codequalityStatus = (state) => { if (state.isLoading) { return LOADING; } @@ -16,7 +16,7 @@ export const codequalityStatus = state => { return SUCCESS; }; -export const codequalityText = state => { +export const codequalityText = (state) => { const { newIssues, resolvedIssues } = state; const text = []; @@ -41,7 +41,7 @@ export const codequalityText = state => { return text.join(''); }; -export const codequalityPopover = state => { +export const codequalityPopover = (state) => { if (state.headPath && !state.basePath) { return { title: s__('ciReport|Base pipeline codequality artifact not found'), diff --git a/app/assets/javascripts/reports/codequality_report/store/index.js b/app/assets/javascripts/reports/codequality_report/store/index.js index 9fa2c589324..5bfcd69edec 100644 --- a/app/assets/javascripts/reports/codequality_report/store/index.js +++ b/app/assets/javascripts/reports/codequality_report/store/index.js @@ -7,11 +7,11 @@ import state from './state'; Vue.use(Vuex); -export const getStoreConfig = initialState => ({ +export const getStoreConfig = (initialState) => ({ actions, getters, mutations, state: state(initialState), }); -export default initialState => new Vuex.Store(getStoreConfig(initialState)); +export default (initialState) => new Vuex.Store(getStoreConfig(initialState)); diff --git a/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js b/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js index eba9e340c4e..fd775f52f7d 100644 --- a/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js +++ b/app/assets/javascripts/reports/codequality_report/store/utils/codequality_comparison.js @@ -1,7 +1,7 @@ import CodeQualityComparisonWorker from '../../workers/codequality_comparison_worker'; export const parseCodeclimateMetrics = (issues = [], path = '') => { - return issues.map(issue => { + return issues.map((issue) => { const parsedIssue = { ...issue, name: issue.description, diff --git a/app/assets/javascripts/reports/codequality_report/workers/codequality_comparison_worker.js b/app/assets/javascripts/reports/codequality_report/workers/codequality_comparison_worker.js index fc55602f95c..ae389d266f8 100644 --- a/app/assets/javascripts/reports/codequality_report/workers/codequality_comparison_worker.js +++ b/app/assets/javascripts/reports/codequality_report/workers/codequality_comparison_worker.js @@ -3,7 +3,7 @@ import { differenceBy } from 'lodash'; const KEY_TO_FILTER_BY = 'fingerprint'; // eslint-disable-next-line no-restricted-globals -self.addEventListener('message', e => { +self.addEventListener('message', (e) => { const { data } = e; if (data === undefined) { diff --git a/app/assets/javascripts/reports/components/grouped_issues_list.vue b/app/assets/javascripts/reports/components/grouped_issues_list.vue index 97587636644..1826fbaddac 100644 --- a/app/assets/javascripts/reports/components/grouped_issues_list.vue +++ b/app/assets/javascripts/reports/components/grouped_issues_list.vue @@ -41,7 +41,7 @@ export default { computed: { groups() { return this.$options.groups - .map(group => ({ + .map((group) => ({ name: group, issues: this[`${group}Issues`], heading: this[`${group}Heading`], diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue index c13df60198b..bf1868d427e 100644 --- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue +++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue @@ -10,7 +10,6 @@ import IssuesList from './issues_list.vue'; import Modal from './modal.vue'; import createStore from '../store'; import Tracking from '~/tracking'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { summaryTextBuilder, reportTextBuilder, @@ -28,7 +27,7 @@ export default { Modal, GlButton, }, - mixins: [glFeatureFlagsMixin(), Tracking.mixin()], + mixins: [Tracking.mixin()], props: { endpoint: { type: String, @@ -44,8 +43,9 @@ export default { computed: { ...mapState(['reports', 'isLoading', 'hasError', 'summary']), ...mapState({ - modalTitle: state => state.modal.title || '', - modalData: state => state.modal.data || {}, + modalTitle: (state) => state.modal.title || '', + modalData: (state) => state.modal.data || {}, + modalOpen: (state) => state.modal.open || false, }), ...mapGetters(['summaryStatus']), groupedSummaryText() { @@ -77,7 +77,7 @@ export default { this.fetchReports(); }, methods: { - ...mapActions(['setEndpoint', 'fetchReports']), + ...mapActions(['setEndpoint', 'fetchReports', 'closeModal']), reportText(report) { const { name, summary } = report || {}; @@ -92,7 +92,7 @@ export default { return reportTextBuilder(name, summary); }, hasRecentFailures(summary) { - return this.glFeatures.testFailureHistory && summary?.recentlyFailed > 0; + return summary?.recentlyFailed > 0; }, recentFailuresText(summary) { return recentFailuresTextBuilder(summary); @@ -171,8 +171,12 @@ export default { class="report-block-group-list" /> </template> - - <modal :title="modalTitle" :modal-data="modalData" /> + <modal + :visible="modalOpen" + :title="modalTitle" + :modal-data="modalData" + @hide="closeModal" + /> </div> </template> </report-section> diff --git a/app/assets/javascripts/reports/components/issues_list.vue b/app/assets/javascripts/reports/components/issues_list.vue index ee07efea3b0..16d5b14d3e9 100644 --- a/app/assets/javascripts/reports/components/issues_list.vue +++ b/app/assets/javascripts/reports/components/issues_list.vue @@ -3,7 +3,7 @@ import ReportItem from '~/reports/components/report_item.vue'; import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants'; import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue'; -const wrapIssueWithState = (status, isNew = false) => issue => ({ +const wrapIssueWithState = (status, isNew = false) => (issue) => ({ status: issue.status || status, isNew, issue, diff --git a/app/assets/javascripts/reports/components/modal.vue b/app/assets/javascripts/reports/components/modal.vue index ca95db6c826..6243bddf941 100644 --- a/app/assets/javascripts/reports/components/modal.vue +++ b/app/assets/javascripts/reports/components/modal.vue @@ -1,15 +1,21 @@ <script> -// import { sprintf, __ } from '~/locale'; -import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue'; +import { GlModal, GlLink, GlSprintf } from '@gitlab/ui'; + import CodeBlock from '~/vue_shared/components/code_block.vue'; import { fieldTypes } from '../constants'; export default { components: { - Modal: DeprecatedModal2, CodeBlock, + GlModal, + GlLink, + GlSprintf, }, props: { + visible: { + type: Boolean, + required: true, + }, title: { type: String, required: true, @@ -23,39 +29,43 @@ export default { }; </script> <template> - <modal - id="modal-mrwidget-reports" - :header-title-text="title" - class="modal-security-report-dast modal-hide-footer" + <gl-modal + :visible="visible" + modal-id="modal-mrwidget-reports" + :title="title" + :hide-footer="true" + @hide="$emit('hide')" > - <slot> - <div - v-for="(field, key, index) in modalData" - v-if="field.value" - :key="index" - class="row gl-mt-3 gl-mb-3" - > - <strong class="col-sm-3 text-right"> {{ field.text }}: </strong> + <div + v-for="(field, key, index) in modalData" + v-if="field.value" + :key="index" + class="row gl-mt-3 gl-mb-3" + > + <strong class="col-sm-3 text-right"> {{ field.text }}: </strong> - <div class="col-sm-9 text-secondary"> - <code-block v-if="field.type === $options.fieldTypes.codeBock" :code="field.value" /> + <div class="col-sm-9 text-secondary"> + <code-block v-if="field.type === $options.fieldTypes.codeBock" :code="field.value" /> - <template v-else-if="field.type === $options.fieldTypes.link"> - <a :href="field.value" target="_blank" rel="noopener noreferrer" class="js-modal-link"> - {{ field.value }} - </a> - </template> + <gl-link + v-else-if="field.type === $options.fieldTypes.link" + :href="field.value" + target="_blank" + > + {{ field.value }} + </gl-link> - <template v-else-if="field.type === $options.fieldTypes.seconds">{{ - sprintf(__('%{value} s'), { value: field.value }) - }}</template> + <gl-sprintf + v-else-if="field.type === $options.fieldTypes.seconds" + :message="__('%{value} s')" + > + <template #value>{{ field.value }}</template> + </gl-sprintf> - <template v-else-if="field.type === $options.fieldTypes.text"> - {{ field.value }} - </template> - </div> + <template v-else-if="field.type === $options.fieldTypes.text"> + {{ field.value }} + </template> </div> - </slot> - <div slot="footer"></div> - </modal> + </div> + </gl-modal> </template> diff --git a/app/assets/javascripts/reports/components/modal_open_name.vue b/app/assets/javascripts/reports/components/modal_open_name.vue deleted file mode 100644 index 78e1fcb205b..00000000000 --- a/app/assets/javascripts/reports/components/modal_open_name.vue +++ /dev/null @@ -1,51 +0,0 @@ -<script> -import { GlTooltipDirective, GlResizeObserverDirective } from '@gitlab/ui'; -import { mapActions } from 'vuex'; - -export default { - directives: { - GlTooltip: GlTooltipDirective, - GlResizeObserverDirective, - }, - props: { - issue: { - type: Object, - required: true, - }, - // failed || success - status: { - type: String, - required: true, - }, - }, - data: () => ({ - tooltipTitle: '', - }), - mounted() { - this.updateTooltipTitle(); - }, - methods: { - ...mapActions(['openModal']), - handleIssueClick() { - const { issue, status, openModal } = this; - openModal({ issue, status }); - }, - updateTooltipTitle() { - // Only show the tooltip if the text is truncated with an ellipsis. - this.tooltipTitle = this.$el.offsetWidth < this.$el.scrollWidth ? this.issue.title : ''; - }, - }, -}; -</script> -<template> - <button - v-gl-tooltip="{ boundary: 'viewport' }" - v-gl-resize-observer-directive="updateTooltipTitle" - class="btn-link gl-text-truncate" - :aria-label="s__('Reports|Vulnerability Name')" - :title="tooltipTitle" - @click="handleIssueClick()" - > - {{ issue.title }} - </button> -</template> diff --git a/app/assets/javascripts/reports/components/report_item.vue b/app/assets/javascripts/reports/components/report_item.vue index 1b47d03aa01..df20d5c19ba 100644 --- a/app/assets/javascripts/reports/components/report_item.vue +++ b/app/assets/javascripts/reports/components/report_item.vue @@ -17,7 +17,7 @@ export default { type: String, required: false, default: '', - validator: value => value === '' || Object.values(componentNames).includes(value), + validator: (value) => value === '' || Object.values(componentNames).includes(value), }, // failed || success status: { diff --git a/app/assets/javascripts/reports/components/report_link.vue b/app/assets/javascripts/reports/components/report_link.vue index f285b526a54..1f68f79e487 100644 --- a/app/assets/javascripts/reports/components/report_link.vue +++ b/app/assets/javascripts/reports/components/report_link.vue @@ -21,16 +21,10 @@ export default { rel="noopener noreferrer nofollow" class="break-link" > - {{ issue.path - }}<template v-if="issue.line" - >:{{ issue.line }}</template - > + {{ issue.path }}<template v-if="issue.line">:{{ issue.line }}</template> </a> <template v-else> - {{ issue.path - }}<template v-if="issue.line" - >:{{ issue.line }}</template - > + {{ issue.path }}<template v-if="issue.line">:{{ issue.line }}</template> </template> </div> </template> diff --git a/app/assets/javascripts/reports/components/test_issue_body.vue b/app/assets/javascripts/reports/components/test_issue_body.vue index 69b0dcf881d..ad980b334bb 100644 --- a/app/assets/javascripts/reports/components/test_issue_body.vue +++ b/app/assets/javascripts/reports/components/test_issue_body.vue @@ -1,7 +1,6 @@ <script> import { mapActions } from 'vuex'; import { GlBadge, GlSprintf } from '@gitlab/ui'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { name: 'TestIssueBody', @@ -9,7 +8,6 @@ export default { GlBadge, GlSprintf, }, - mixins: [glFeatureFlagsMixin()], props: { issue: { type: Object, @@ -28,11 +26,7 @@ export default { }, computed: { showRecentFailures() { - return ( - this.glFeatures.testFailureHistory && - this.issue.recent_failures?.count && - this.issue.recent_failures?.base_branch - ); + return this.issue.recent_failures?.count && this.issue.recent_failures?.base_branch; }, }, methods: { diff --git a/app/assets/javascripts/reports/store/actions.js b/app/assets/javascripts/reports/store/actions.js index c5860db6601..301fdce7989 100644 --- a/app/assets/javascripts/reports/store/actions.js +++ b/app/assets/javascripts/reports/store/actions.js @@ -1,5 +1,4 @@ import Visibility from 'visibilityjs'; -import $ from 'jquery'; import axios from '../../lib/utils/axios_utils'; import Poll from '../../lib/utils/poll'; import * as types from './mutation_types'; @@ -78,10 +77,6 @@ export const receiveReportsSuccess = ({ commit }, response) => { export const receiveReportsError = ({ commit }) => commit(types.RECEIVE_REPORTS_ERROR); -export const openModal = ({ dispatch }, payload) => { - dispatch('setModalData', payload); +export const openModal = ({ commit }, payload) => commit(types.SET_ISSUE_MODAL_DATA, payload); - $('#modal-mrwidget-reports').modal('show'); -}; - -export const setModalData = ({ commit }, payload) => commit(types.SET_ISSUE_MODAL_DATA, payload); +export const closeModal = ({ commit }, payload) => commit(types.RESET_ISSUE_MODAL_DATA, payload); diff --git a/app/assets/javascripts/reports/store/getters.js b/app/assets/javascripts/reports/store/getters.js index d49e5760b3f..cc8c4cc446c 100644 --- a/app/assets/javascripts/reports/store/getters.js +++ b/app/assets/javascripts/reports/store/getters.js @@ -1,6 +1,6 @@ import { LOADING, ERROR, SUCCESS, STATUS_FAILED } from '../constants'; -export const summaryStatus = state => { +export const summaryStatus = (state) => { if (state.isLoading) { return LOADING; } diff --git a/app/assets/javascripts/reports/store/mutation_types.js b/app/assets/javascripts/reports/store/mutation_types.js index 599d4862dfe..337085f9bf0 100644 --- a/app/assets/javascripts/reports/store/mutation_types.js +++ b/app/assets/javascripts/reports/store/mutation_types.js @@ -4,3 +4,4 @@ export const REQUEST_REPORTS = 'REQUEST_REPORTS'; export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS'; export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR'; export const SET_ISSUE_MODAL_DATA = 'SET_ISSUE_MODAL_DATA'; +export const RESET_ISSUE_MODAL_DATA = 'RESET_ISSUE_MODAL_DATA'; diff --git a/app/assets/javascripts/reports/store/mutations.js b/app/assets/javascripts/reports/store/mutations.js index acaa98754b0..3bb31d71d8f 100644 --- a/app/assets/javascripts/reports/store/mutations.js +++ b/app/assets/javascripts/reports/store/mutations.js @@ -9,7 +9,7 @@ export default { state.isLoading = true; }, [types.RECEIVE_REPORTS_SUCCESS](state, response) { - state.hasError = response.suites.some(suite => suite.status === 'error'); + state.hasError = response.suites.some((suite) => suite.status === 'error'); state.isLoading = false; @@ -44,7 +44,7 @@ export default { [types.SET_ISSUE_MODAL_DATA](state, payload) { state.modal.title = payload.issue.name; - Object.keys(payload.issue).forEach(key => { + Object.keys(payload.issue).forEach((key) => { if (Object.prototype.hasOwnProperty.call(state.modal.data, key)) { state.modal.data[key] = { ...state.modal.data[key], @@ -52,5 +52,19 @@ export default { }; } }); + + state.modal.open = true; + }, + [types.RESET_ISSUE_MODAL_DATA](state) { + state.modal.open = false; + + // Resetting modal data + state.modal.title = null; + Object.keys(state.modal.data).forEach((key) => { + state.modal.data[key] = { + ...state.modal.data[key], + value: null, + }; + }); }, }; diff --git a/app/assets/javascripts/reports/store/state.js b/app/assets/javascripts/reports/store/state.js index 4f9eb53e787..e8a0db2e1a8 100644 --- a/app/assets/javascripts/reports/store/state.js +++ b/app/assets/javascripts/reports/store/state.js @@ -38,6 +38,7 @@ export default () => ({ modal: { title: null, + open: false, data: { class: { diff --git a/app/assets/javascripts/reports/store/utils.js b/app/assets/javascripts/reports/store/utils.js index 2d32daee9d0..d89833032a0 100644 --- a/app/assets/javascripts/reports/store/utils.js +++ b/app/assets/javascripts/reports/store/utils.js @@ -7,7 +7,7 @@ import { ICON_NOTFOUND, } from '../constants'; -const textBuilder = results => { +const textBuilder = (results) => { const { failed, errored, resolved, total } = results; const failedOrErrored = (failed || 0) + (errored || 0); @@ -70,18 +70,18 @@ export const recentFailuresTextBuilder = (summary = {}) => { ); }; -export const countRecentlyFailedTests = subject => { +export const countRecentlyFailedTests = (subject) => { // handle either a single report or an array of reports const reports = !subject.length ? [subject] : subject; return reports - .map(report => { + .map((report) => { return ( [report.new_failures, report.existing_failures, report.resolved_failures] // only count tests which have failed more than once .map( - failureArray => - failureArray.filter(failure => failure.recent_failures?.count > 1).length, + (failureArray) => + failureArray.filter((failure) => failure.recent_failures?.count > 1).length, ) .reduce((total, count) => total + count, 0) ); @@ -89,7 +89,7 @@ export const countRecentlyFailedTests = subject => { .reduce((total, count) => total + count, 0); }; -export const statusIcon = status => { +export const statusIcon = (status) => { if (status === STATUS_FAILED) { return ICON_WARNING; } diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue index a1f1c77df2f..c9862572b16 100644 --- a/app/assets/javascripts/repository/components/breadcrumbs.vue +++ b/app/assets/javascripts/repository/components/breadcrumbs.vue @@ -40,7 +40,7 @@ export default { projectPath: this.projectPath, }; }, - update: data => data.project?.userPermissions, + update: (data) => data.project?.userPermissions, error(error) { throw error; }, @@ -105,7 +105,7 @@ export default { pathLinks() { return this.currentPath .split('/') - .filter(p => p !== '') + .filter((p) => p !== '') .reduce( (acc, name, i) => { const path = joinPaths(i > 0 ? acc[i].path : '', escapeFileUrl(name)); diff --git a/app/assets/javascripts/repository/components/directory_download_links.vue b/app/assets/javascripts/repository/components/directory_download_links.vue index dffadade082..8c029fc9973 100644 --- a/app/assets/javascripts/repository/components/directory_download_links.vue +++ b/app/assets/javascripts/repository/components/directory_download_links.vue @@ -18,7 +18,7 @@ export default { }, computed: { normalizedLinks() { - return this.links.map(link => ({ + return this.links.map((link) => ({ text: link.text, path: `${link.path}?path=${this.currentPath}`, })); diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue index 2626bace363..0241c803514 100644 --- a/app/assets/javascripts/repository/components/last_commit.vue +++ b/app/assets/javascripts/repository/components/last_commit.vue @@ -39,7 +39,7 @@ export default { path: this.currentPath.replace(/^\//, ''), }; }, - update: data => { + update: (data) => { const pipelines = data.project?.repository?.tree?.lastCommit?.pipelines?.edges; return { @@ -81,6 +81,10 @@ export default { showCommitId() { return this.commit?.sha?.substr(0, 8); }, + commitDescription() { + // Strip the newline at the beginning + return this.commit?.descriptionHtml?.replace(/^
/, ''); + }, }, watch: { currentPath() { @@ -146,10 +150,10 @@ export default { <timeago-tooltip :time="commit.authoredDate" tooltip-placement="bottom" /> </div> <pre - v-if="commit.descriptionHtml" + v-if="commitDescription" :class="{ 'd-block': showDescription }" class="commit-row-description gl-mb-3" - v-html="commit.descriptionHtml" + v-html="commitDescription" ></pre> </div> <div class="commit-actions flex-row"> diff --git a/app/assets/javascripts/repository/components/table/parent_row.vue b/app/assets/javascripts/repository/components/table/parent_row.vue index b4095e00884..fb0e505a16e 100644 --- a/app/assets/javascripts/repository/components/table/parent_row.vue +++ b/app/assets/javascripts/repository/components/table/parent_row.vue @@ -25,7 +25,7 @@ export default { const splitArray = this.path.split('/'); splitArray.pop(); - return splitArray.map(p => encodeURIComponent(p)).join('/'); + return splitArray.map((p) => encodeURIComponent(p)).join('/'); }, parentRoute() { return { path: `/-/tree/${this.commitRef}/${this.parentPath}` }; @@ -48,9 +48,7 @@ export default { inline class="d-inline-block align-text-bottom" /> - <router-link v-else :to="parentRoute" :aria-label="__('Go to parent')"> - .. - </router-link> + <router-link v-else :to="parentRoute" :aria-label="__('Go to parent')"> .. </router-link> </td> </tr> </template> diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue index b42f88631b5..7fe6863d006 100644 --- a/app/assets/javascripts/repository/components/tree_content.vue +++ b/app/assets/javascripts/repository/components/tree_content.vue @@ -113,7 +113,7 @@ export default { } } }) - .catch(error => { + .catch((error) => { createFlash(__('An error occurred while fetching folder content.')); throw error; }); diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js index 8dd18027945..dbe129859bc 100644 --- a/app/assets/javascripts/repository/graphql.js +++ b/app/assets/javascripts/repository/graphql.js @@ -18,7 +18,7 @@ const defaultClient = createDefaultClient( { Query: { commit(_, { path, fileName, type }) { - return new Promise(resolve => { + return new Promise((resolve) => { fetchLogsTree(defaultClient, path, '0', { resolve, entry: { @@ -38,7 +38,7 @@ const defaultClient = createDefaultClient( { cacheConfig: { fragmentMatcher, - dataIdFromObject: obj => { + dataIdFromObject: (obj) => { /* eslint-disable @gitlab/require-i18n-strings */ // eslint-disable-next-line no-underscore-dangle switch (obj.__typename) { diff --git a/app/assets/javascripts/repository/log_tree.js b/app/assets/javascripts/repository/log_tree.js index fc8fa40a855..9001bcd8fc3 100644 --- a/app/assets/javascripts/repository/log_tree.js +++ b/app/assets/javascripts/repository/log_tree.js @@ -9,7 +9,9 @@ const fetchpromises = {}; const resolvers = {}; export function resolveCommit(commits, path, { resolve, entry }) { - const commit = commits.find(c => c.filePath === `${path}/${entry.name}` && c.type === entry.type); + const commit = commits.find( + (c) => c.filePath === `${path}/${entry.name}` && c.type === entry.type, + ); if (commit) { resolve(commit); @@ -42,7 +44,7 @@ export function fetchLogsTree(client, path, offset, resolver = null) { .then(({ data: newData, headers }) => { const headerLogsOffset = headers['more-logs-offset']; const sourceData = client.readQuery({ query: commitsQuery }); - const data = produce(sourceData, draftState => { + const data = produce(sourceData, (draftState) => { draftState.commits.push(...normalizeData(newData, path)); }); client.writeQuery({ @@ -50,7 +52,7 @@ export function fetchLogsTree(client, path, offset, resolver = null) { data, }); - resolvers[path].forEach(r => resolveCommit(data.commits, path, r)); + resolvers[path].forEach((r) => resolveCommit(data.commits, path, r)); delete fetchpromises[path]; diff --git a/app/assets/javascripts/repository/queries/project_path.query.graphql b/app/assets/javascripts/repository/queries/project_path.query.graphql index 74e73e07577..9e5c10b3de3 100644 --- a/app/assets/javascripts/repository/queries/project_path.query.graphql +++ b/app/assets/javascripts/repository/queries/project_path.query.graphql @@ -1,3 +1,3 @@ query getProjectPath { - projectPath + projectPath @client } diff --git a/app/assets/javascripts/repository/router.js b/app/assets/javascripts/repository/router.js index 38a596e229e..e2924454239 100644 --- a/app/assets/javascripts/repository/router.js +++ b/app/assets/javascripts/repository/router.js @@ -10,7 +10,7 @@ Vue.use(VueRouter); export default function createRouter(base, baseRef) { const treePathRoute = { component: TreePage, - props: route => ({ + props: (route) => ({ path: route.params.path?.replace(/^\//, '') || '/', }), }; diff --git a/app/assets/javascripts/repository/utils/commit.js b/app/assets/javascripts/repository/utils/commit.js index 0704ac1627f..a67252ec004 100644 --- a/app/assets/javascripts/repository/utils/commit.js +++ b/app/assets/javascripts/repository/utils/commit.js @@ -1,5 +1,5 @@ export function normalizeData(data, path, extra = () => {}) { - return data.map(d => ({ + return data.map((d) => ({ sha: d.commit.id, message: d.commit.message, titleHtml: d.commit_title_html, diff --git a/app/assets/javascripts/repository/utils/dom.js b/app/assets/javascripts/repository/utils/dom.js index abf726194ac..b667cd53b18 100644 --- a/app/assets/javascripts/repository/utils/dom.js +++ b/app/assets/javascripts/repository/utils/dom.js @@ -1,7 +1,9 @@ import { joinPaths } from '~/lib/utils/url_utility'; export const updateElementsVisibility = (selector, isVisible) => { - document.querySelectorAll(selector).forEach(elem => elem.classList.toggle('hidden', !isVisible)); + document + .querySelectorAll(selector) + .forEach((elem) => elem.classList.toggle('hidden', !isVisible)); }; export const updateFormAction = (selector, basePath, path) => { diff --git a/app/assets/javascripts/repository/utils/readme.js b/app/assets/javascripts/repository/utils/readme.js index 50692779b1a..c666cd95038 100644 --- a/app/assets/javascripts/repository/utils/readme.js +++ b/app/assets/javascripts/repository/utils/readme.js @@ -18,14 +18,14 @@ const MARKUP_EXTENSIONS = [ 'wiki', ]; -const isRichReadme = file => { +const isRichReadme = (file) => { const re = new RegExp(`^(${FILENAMES.join('|')})\\.(${MARKUP_EXTENSIONS.join('|')})$`, 'i'); return re.test(file.name); }; -const isPlainReadme = file => { +const isPlainReadme = (file) => { const re = new RegExp(`^(${FILENAMES.join('|')})(\\.txt)?$`, 'i'); return re.test(file.name); }; -export const readmeFile = blobs => blobs.find(isRichReadme) || blobs.find(isPlainReadme); +export const readmeFile = (blobs) => blobs.find(isRichReadme) || blobs.find(isPlainReadme); diff --git a/app/assets/javascripts/rest_api.js b/app/assets/javascripts/rest_api.js new file mode 100644 index 00000000000..ea8f87001f0 --- /dev/null +++ b/app/assets/javascripts/rest_api.js @@ -0,0 +1,15 @@ +export * from './api/groups_api'; +export * from './api/projects_api'; +export * from './api/user_api'; + +// Note: It's not possible to spy on methods imported from this file in +// Jest tests. See https://stackoverflow.com/a/53307822/1063392. +// As a workaround, in Jest tests, import the methods from the file +// in which they are defined: +// +// import * as UserApi from '~/api/user_api'; +// vs... +// import * as UserApi from '~/rest_api'; +// +// // This will only work with option #2 above. +// jest.spyOn(UserApi, 'getUsers') diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 6f43f837374..b9bc799fb0b 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -15,13 +15,13 @@ function Sidebar() { this.addEventListeners(); } -Sidebar.initialize = function() { +Sidebar.initialize = function () { if (!this.instance) { this.instance = new Sidebar(); } }; -Sidebar.prototype.removeListeners = function() { +Sidebar.prototype.removeListeners = function () { this.sidebar.off('click', '.sidebar-collapsed-icon'); // eslint-disable-next-line @gitlab/no-global-event-off this.sidebar.off('hidden.gl.dropdown'); @@ -32,7 +32,7 @@ Sidebar.prototype.removeListeners = function() { $(document).off('click', '.js-sidebar-toggle'); }; -Sidebar.prototype.addEventListeners = function() { +Sidebar.prototype.addEventListeners = function () { const $document = $(document); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); @@ -44,7 +44,7 @@ Sidebar.prototype.addEventListeners = function() { .on('click', '.js-issuable-todo', this.toggleTodo); }; -Sidebar.prototype.sidebarToggleClicked = function(e, triggered) { +Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { const $this = $(this); const $collapseIcon = $('.js-sidebar-collapse'); const $expandIcon = $('.js-sidebar-expand'); @@ -60,9 +60,7 @@ Sidebar.prototype.sidebarToggleClicked = function(e, triggered) { $('aside.right-sidebar') .removeClass('right-sidebar-expanded') .addClass('right-sidebar-collapsed'); - $('.layout-page') - .removeClass('right-sidebar-expanded') - .addClass('right-sidebar-collapsed'); + $('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); } else { $toggleContainer.data('is-expanded', true); $expandIcon.addClass('hidden'); @@ -70,9 +68,7 @@ Sidebar.prototype.sidebarToggleClicked = function(e, triggered) { $('aside.right-sidebar') .removeClass('right-sidebar-collapsed') .addClass('right-sidebar-expanded'); - $('.layout-page') - .removeClass('right-sidebar-collapsed') - .addClass('right-sidebar-expanded'); + $('.layout-page').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); } $this.attr('data-original-title', tooltipLabel); @@ -82,16 +78,14 @@ Sidebar.prototype.sidebarToggleClicked = function(e, triggered) { } }; -Sidebar.prototype.toggleTodo = function(e) { +Sidebar.prototype.toggleTodo = function (e) { const $this = $(e.currentTarget); const ajaxType = $this.data('deletePath') ? 'delete' : 'post'; const url = String($this.data('deletePath') || $this.data('createPath')); hide($this); - $('.js-issuable-todo') - .disable() - .addClass('is-loading'); + $('.js-issuable-todo').disable().addClass('is-loading'); axios[ajaxType](url, { issuable_id: $this.data('issuableId'), @@ -110,7 +104,7 @@ Sidebar.prototype.toggleTodo = function(e) { ); }; -Sidebar.prototype.todoUpdateDone = function(data) { +Sidebar.prototype.todoUpdateDone = function (data) { const deletePath = data.delete_path ? data.delete_path : null; const attrPrefix = deletePath ? 'mark' : 'todo'; const $todoBtns = $('.js-issuable-todo'); @@ -140,7 +134,7 @@ Sidebar.prototype.todoUpdateDone = function(data) { }); }; -Sidebar.prototype.sidebarCollapseClicked = function(e) { +Sidebar.prototype.sidebarCollapseClicked = function (e) { if ($(e.currentTarget).hasClass('dont-change-state')) { return; } @@ -150,7 +144,7 @@ Sidebar.prototype.sidebarCollapseClicked = function(e) { return sidebar.openDropdown($block); }; -Sidebar.prototype.openDropdown = function(blockOrName) { +Sidebar.prototype.openDropdown = function (blockOrName) { const $block = typeof blockOrName === 'string' ? this.getBlock(blockOrName) : blockOrName; if (!this.isOpen()) { this.setCollapseAfterUpdate($block); @@ -164,19 +158,19 @@ Sidebar.prototype.openDropdown = function(blockOrName) { }); }; -Sidebar.prototype.setCollapseAfterUpdate = function($block) { +Sidebar.prototype.setCollapseAfterUpdate = function ($block) { $block.addClass('collapse-after-update'); return $('.layout-page').addClass('with-overlay'); }; -Sidebar.prototype.onSidebarDropdownHidden = function(e) { +Sidebar.prototype.onSidebarDropdownHidden = function (e) { const sidebar = e.data; e.preventDefault(); const $block = $(e.target).closest('.block'); return sidebar.sidebarDropdownHidden($block); }; -Sidebar.prototype.sidebarDropdownHidden = function($block) { +Sidebar.prototype.sidebarDropdownHidden = function ($block) { if ($block.hasClass('collapse-after-update')) { $block.removeClass('collapse-after-update'); $('.layout-page').removeClass('with-overlay'); @@ -184,11 +178,11 @@ Sidebar.prototype.sidebarDropdownHidden = function($block) { } }; -Sidebar.prototype.triggerOpenSidebar = function() { +Sidebar.prototype.triggerOpenSidebar = function () { return this.sidebar.find('.js-sidebar-toggle').trigger('click'); }; -Sidebar.prototype.toggleSidebar = function(action) { +Sidebar.prototype.toggleSidebar = function (action) { if (action == null) { action = 'toggle'; } @@ -207,11 +201,11 @@ Sidebar.prototype.toggleSidebar = function(action) { } }; -Sidebar.prototype.isOpen = function() { +Sidebar.prototype.isOpen = function () { return this.sidebar.is('.right-sidebar-expanded'); }; -Sidebar.prototype.getBlock = function(name) { +Sidebar.prototype.getBlock = function (name) { return this.sidebar.find(`.block.${name}`); }; diff --git a/app/assets/javascripts/search/highlight_blob_search_result.js b/app/assets/javascripts/search/highlight_blob_search_result.js index e17c87735b4..3c3ac3582d0 100644 --- a/app/assets/javascripts/search/highlight_blob_search_result.js +++ b/app/assets/javascripts/search/highlight_blob_search_result.js @@ -4,9 +4,9 @@ export default () => { const searchTerm = contentBody.querySelector('.js-search-input').value.toLowerCase(); const blobs = contentBody.querySelectorAll('.blob-result'); - blobs.forEach(blob => { + blobs.forEach((blob) => { const lines = blob.querySelectorAll('.line'); - lines.forEach(line => { + lines.forEach((line) => { if (line.textContent.toLowerCase().includes(searchTerm)) { line.classList.add(highlightLineClass); } diff --git a/app/assets/javascripts/search/sidebar/index.js b/app/assets/javascripts/search/sidebar/index.js index 6419e8ac2c6..1414adcac27 100644 --- a/app/assets/javascripts/search/sidebar/index.js +++ b/app/assets/javascripts/search/sidebar/index.js @@ -4,7 +4,7 @@ import GlobalSearchSidebar from './components/app.vue'; Vue.use(Translate); -export const initSidebar = store => { +export const initSidebar = (store) => { const el = document.getElementById('js-search-sidebar'); if (!el) return false; diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js index 082beb5930d..bdfe966d990 100644 --- a/app/assets/javascripts/search/store/actions.js +++ b/app/assets/javascripts/search/store/actions.js @@ -7,7 +7,7 @@ import * as types from './mutation_types'; export const fetchGroups = ({ commit }, search) => { commit(types.REQUEST_GROUPS); Api.groups(search) - .then(data => { + .then((data) => { commit(types.RECEIVE_GROUPS_SUCCESS, data); }) .catch(() => { @@ -19,7 +19,7 @@ export const fetchGroups = ({ commit }, search) => { export const fetchProjects = ({ commit, state }, search) => { commit(types.REQUEST_PROJECTS); const groupId = state.query?.group_id; - const callback = data => { + const callback = (data) => { if (data) { commit(types.RECEIVE_PROJECTS_SUCCESS, data); } else { diff --git a/app/assets/javascripts/search/store/index.js b/app/assets/javascripts/search/store/index.js index e0a7e488f9f..1923c8b96ab 100644 --- a/app/assets/javascripts/search/store/index.js +++ b/app/assets/javascripts/search/store/index.js @@ -12,5 +12,5 @@ export const getStoreConfig = ({ query }) => ({ state: createState({ query }), }); -const createStore = config => new Vuex.Store(getStoreConfig(config)); +const createStore = (config) => new Vuex.Store(getStoreConfig(config)); export default createStore; diff --git a/app/assets/javascripts/search/topbar/index.js b/app/assets/javascripts/search/topbar/index.js index 024544148a0..f0308109b32 100644 --- a/app/assets/javascripts/search/topbar/index.js +++ b/app/assets/javascripts/search/topbar/index.js @@ -40,5 +40,5 @@ const searchableDropdowns = [ }, ]; -export const initTopbar = store => - searchableDropdowns.map(dropdown => mountSearchableDropdown(store, dropdown)); +export const initTopbar = (store) => + searchableDropdowns.map((dropdown) => mountSearchableDropdown(store, dropdown)); diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 97674348436..b8a5836e2d4 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -76,8 +76,8 @@ export class SearchAutocomplete { this.wrap = wrap || $('.search'); this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts'); this.autocompletePath = autocompletePath || this.optsEl.data('autocompletePath'); - this.projectId = projectId || (this.optsEl.data('autocompleteProjectId') || ''); - this.projectRef = projectRef || (this.optsEl.data('autocompleteProjectRef') || ''); + this.projectId = projectId || this.optsEl.data('autocompleteProjectId') || ''; + this.projectRef = projectRef || this.optsEl.data('autocompleteProjectRef') || ''; this.dropdown = this.wrap.find('.dropdown'); this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle'); this.dropdownMenu = this.dropdown.find('.dropdown-menu'); @@ -172,7 +172,7 @@ export class SearchAutocomplete { term, }, }) - .then(response => { + .then((response) => { const options = this.scopedSearchOptions(term); // List results @@ -345,7 +345,7 @@ export class SearchAutocomplete { this.clearInput.on('click', this.onClearInputClick); this.dropdownContent.on('scroll', throttle(this.setScrollFade, 250)); - this.searchInput.on('click', e => { + this.searchInput.on('click', (e) => { e.stopPropagation(); }); } diff --git a/app/assets/javascripts/search_settings/components/search_settings.vue b/app/assets/javascripts/search_settings/components/search_settings.vue new file mode 100644 index 00000000000..820055dc656 --- /dev/null +++ b/app/assets/javascripts/search_settings/components/search_settings.vue @@ -0,0 +1,129 @@ +<script> +import { GlSearchBoxByType } from '@gitlab/ui'; +import { uniq } from 'lodash'; +import { EXCLUDED_NODES, HIDE_CLASS, HIGHLIGHT_CLASS, TYPING_DELAY } from '../constants'; + +const findSettingsSection = (sectionSelector, node) => { + return node.parentElement.closest(sectionSelector); +}; + +const resetSections = ({ sectionSelector, expandSection, collapseSection }) => { + document.querySelectorAll(sectionSelector).forEach((section, index) => { + section.classList.remove(HIDE_CLASS); + + if (index === 0) { + expandSection(section); + } else { + collapseSection(section); + } + }); +}; + +const clearHighlights = () => { + document + .querySelectorAll(`.${HIGHLIGHT_CLASS}`) + .forEach((element) => element.classList.remove(HIGHLIGHT_CLASS)); +}; + +const hideSectionsExcept = (sectionSelector, visibleSections) => { + Array.from(document.querySelectorAll(sectionSelector)) + .filter((section) => !visibleSections.includes(section)) + .forEach((section) => { + section.classList.add(HIDE_CLASS); + }); +}; + +const highlightElements = (elements = []) => { + elements.forEach((element) => element.classList.add(HIGHLIGHT_CLASS)); +}; + +const displayResults = ({ sectionSelector, expandSection }, matches) => { + const elements = matches.map((match) => match.parentElement); + const sections = uniq(elements.map((element) => findSettingsSection(sectionSelector, element))); + + hideSectionsExcept(sectionSelector, sections); + sections.forEach(expandSection); + highlightElements(elements); +}; + +const clearResults = (params) => { + resetSections(params); + clearHighlights(); +}; + +const includeNode = (node, lowerSearchTerm) => + node.textContent.toLowerCase().includes(lowerSearchTerm) && + EXCLUDED_NODES.every((excluded) => !node.parentElement.closest(excluded)); + +const search = (root, searchTerm) => { + const iterator = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, { + acceptNode(node) { + return includeNode(node, searchTerm.toLowerCase()) + ? NodeFilter.FILTER_ACCEPT + : NodeFilter.FILTER_REJECT; + }, + }); + const results = []; + + for (let currentNode = iterator.nextNode(); currentNode; currentNode = iterator.nextNode()) { + results.push(currentNode); + } + + return results; +}; + +export default { + components: { + GlSearchBoxByType, + }, + props: { + searchRoot: { + type: Element, + required: true, + }, + sectionSelector: { + type: String, + required: true, + }, + }, + data() { + return { + searchTerm: '', + }; + }, + methods: { + search(value) { + const displayOptions = { + sectionSelector: this.sectionSelector, + expandSection: this.expandSection, + collapseSection: this.collapseSection, + }; + + this.searchTerm = value; + + clearResults(displayOptions); + + if (value.length) { + displayResults(displayOptions, search(this.searchRoot, value)); + } + }, + expandSection(section) { + this.$emit('expand', section); + }, + collapseSection(section) { + this.$emit('collapse', section); + }, + }, + TYPING_DELAY, +}; +</script> +<template> + <div class="gl-mt-5"> + <gl-search-box-by-type + :value="searchTerm" + :debounce="$options.TYPING_DELAY" + :placeholder="__('Search settings')" + @input="search" + /> + </div> +</template> diff --git a/app/assets/javascripts/search_settings/constants.js b/app/assets/javascripts/search_settings/constants.js new file mode 100644 index 00000000000..499e42854ed --- /dev/null +++ b/app/assets/javascripts/search_settings/constants.js @@ -0,0 +1,11 @@ +// We do not consider these nodes in the search index +export const EXCLUDED_NODES = ['OPTION']; + +// Used to hide the sections that do not match * the search term +export const HIDE_CLASS = 'gl-display-none'; + +// used to highlight the text that matches the * search term +export const HIGHLIGHT_CLASS = 'gl-bg-orange-50'; + +// How many seconds to wait until the user * stops typing +export const TYPING_DELAY = 400; diff --git a/app/assets/javascripts/search_settings/index.js b/app/assets/javascripts/search_settings/index.js new file mode 100644 index 00000000000..1fb1a378ffb --- /dev/null +++ b/app/assets/javascripts/search_settings/index.js @@ -0,0 +1,23 @@ +import Vue from 'vue'; +import $ from 'jquery'; +import { expandSection, closeSection } from '~/settings_panels'; +import SearchSettings from '~/search_settings/components/search_settings.vue'; + +const initSearch = ({ el }) => + new Vue({ + el, + render: (h) => + h(SearchSettings, { + ref: 'searchSettings', + props: { + searchRoot: document.querySelector('#content-body'), + sectionSelector: 'section.settings', + }, + on: { + collapse: (section) => closeSection($(section)), + expand: (section) => expandSection($(section)), + }, + }), + }); + +export default initSearch; diff --git a/app/assets/javascripts/self_monitor/store/actions.js b/app/assets/javascripts/self_monitor/store/actions.js index 10deec6921c..99731309a62 100644 --- a/app/assets/javascripts/self_monitor/store/actions.js +++ b/app/assets/javascripts/self_monitor/store/actions.js @@ -9,7 +9,7 @@ const TWO_MINUTES = 120000; function backOffRequest(makeRequestCallback) { return backOff((next, stop) => { makeRequestCallback() - .then(resp => { + .then((resp) => { if (resp.status === statusCodes.ACCEPTED) { next(); } else { @@ -30,24 +30,24 @@ export const requestCreateProject = ({ dispatch, state, commit }) => { commit(types.SET_LOADING, true); axios .post(state.createProjectEndpoint) - .then(resp => { + .then((resp) => { if (resp.status === statusCodes.ACCEPTED) { dispatch('requestCreateProjectStatus', resp.data.job_id); } }) - .catch(error => { + .catch((error) => { dispatch('requestCreateProjectError', error); }); }; export const requestCreateProjectStatus = ({ dispatch, state }, jobId) => { backOffRequest(() => axios.get(state.createProjectStatusEndpoint, { params: { job_id: jobId } })) - .then(resp => { + .then((resp) => { if (resp.status === statusCodes.OK) { dispatch('requestCreateProjectSuccess', resp.data); } }) - .catch(error => { + .catch((error) => { dispatch('requestCreateProjectError', error); }); }; @@ -82,24 +82,24 @@ export const requestDeleteProject = ({ dispatch, state, commit }) => { commit(types.SET_LOADING, true); axios .delete(state.deleteProjectEndpoint) - .then(resp => { + .then((resp) => { if (resp.status === statusCodes.ACCEPTED) { dispatch('requestDeleteProjectStatus', resp.data.job_id); } }) - .catch(error => { + .catch((error) => { dispatch('requestDeleteProjectError', error); }); }; export const requestDeleteProjectStatus = ({ dispatch, state }, jobId) => { backOffRequest(() => axios.get(state.deleteProjectStatusEndpoint, { params: { job_id: jobId } })) - .then(resp => { + .then((resp) => { if (resp.status === statusCodes.OK) { dispatch('requestDeleteProjectSuccess', resp.data); } }) - .catch(error => { + .catch((error) => { dispatch('requestDeleteProjectError', error); }); }; diff --git a/app/assets/javascripts/self_monitor/store/index.js b/app/assets/javascripts/self_monitor/store/index.js index a222e9c87b8..1144fff759a 100644 --- a/app/assets/javascripts/self_monitor/store/index.js +++ b/app/assets/javascripts/self_monitor/store/index.js @@ -6,7 +6,7 @@ import mutations from './mutations'; Vue.use(Vuex); -export const createStore = initialState => +export const createStore = (initialState) => new Vuex.Store({ modules: { selfMonitoring: { diff --git a/app/assets/javascripts/sentry_error_stack_trace/index.js b/app/assets/javascripts/sentry_error_stack_trace/index.js index 9b24ddc335d..80fa0988f0a 100644 --- a/app/assets/javascripts/sentry_error_stack_trace/index.js +++ b/app/assets/javascripts/sentry_error_stack_trace/index.js @@ -13,7 +13,7 @@ export default function initSentryErrorStacktrace() { SentryErrorStackTrace, }, store, - render: createElement => + render: (createElement) => createElement('sentry-error-stack-trace', { props: { issueStackTracePath }, }), diff --git a/app/assets/javascripts/serverless/components/area.vue b/app/assets/javascripts/serverless/components/area.vue index 71f2e948917..056b342cf39 100644 --- a/app/assets/javascripts/serverless/components/area.vue +++ b/app/assets/javascripts/serverless/components/area.vue @@ -39,13 +39,13 @@ export default { }, {}); }, extractTimeData() { - return this.chartData.requests.map(data => data.time); + return this.chartData.requests.map((data) => data.time); }, generateSeries() { return { name: __('Invocations'), type: 'line', - data: this.chartData.requests.map(data => [data.time, data.value]), + data: this.chartData.requests.map((data) => [data.time, data.value]), symbolSize: 0, }; }, @@ -69,7 +69,7 @@ export default { name: 'time', type: 'time', axisLabel: { - formatter: date => dateFormat(date, 'h:MM TT'), + formatter: (date) => dateFormat(date, 'h:MM TT'), }, data: this.extractTimeData, nameTextStyle: { @@ -90,7 +90,7 @@ export default { }; }, xAxisLabel() { - return this.graphData.queries.map(query => query.label).join(', '); + return this.graphData.queries.map((query) => query.label).join(', '); }, yAxisLabel() { const [query] = this.graphData.queries; diff --git a/app/assets/javascripts/serverless/store/actions.js b/app/assets/javascripts/serverless/store/actions.js index b9d57138efa..acd7020f70f 100644 --- a/app/assets/javascripts/serverless/store/actions.js +++ b/app/assets/javascripts/serverless/store/actions.js @@ -30,7 +30,7 @@ export const receiveMetricsError = ({ commit }, error) => export const fetchFunctions = ({ dispatch }, { functionsPath }) => { let retryCount = 0; - const functionsPartiallyFetched = data => { + const functionsPartiallyFetched = (data) => { if (data.functions !== null && data.functions.length) { dispatch('receiveFunctionsPartial', data); } @@ -41,7 +41,7 @@ export const fetchFunctions = ({ dispatch }, { functionsPath }) => { backOff((next, stop) => { axios .get(functionsPath) - .then(response => { + .then((response) => { if (response.data.knative_installed === CHECKING_INSTALLED) { retryCount += 1; if (retryCount < MAX_REQUESTS) { @@ -56,7 +56,7 @@ export const fetchFunctions = ({ dispatch }, { functionsPath }) => { }) .catch(stop); }) - .then(data => { + .then((data) => { if (data === TIMEOUT) { dispatch('receiveFunctionsTimeout'); createFlash(__('Loading functions timed out. Please reload the page to try again.')); @@ -66,7 +66,7 @@ export const fetchFunctions = ({ dispatch }, { functionsPath }) => { dispatch('receiveFunctionsNoDataSuccess', data); } }) - .catch(error => { + .catch((error) => { dispatch('receiveFunctionsError', error); createFlash(error); }); @@ -83,7 +83,7 @@ export const fetchMetrics = ({ dispatch }, { metricsPath, hasPrometheus }) => { backOff((next, stop) => { axios .get(metricsPath) - .then(response => { + .then((response) => { if (response.status === statusCodes.NO_CONTENT) { retryCount += 1; if (retryCount < MAX_REQUESTS) { @@ -98,15 +98,15 @@ export const fetchMetrics = ({ dispatch }, { metricsPath, hasPrometheus }) => { }) .catch(stop); }) - .then(data => { + .then((data) => { if (data === null) { return; } const updatedMetric = data.metrics; - const queries = data.metrics.queries.map(query => ({ + const queries = data.metrics.queries.map((query) => ({ ...query, - result: query.result.map(result => ({ + result: query.result.map((result) => ({ ...result, values: result.values.map(([timestamp, value]) => ({ time: new Date(timestamp * 1000).toISOString(), @@ -118,7 +118,7 @@ export const fetchMetrics = ({ dispatch }, { metricsPath, hasPrometheus }) => { updatedMetric.queries = queries; dispatch('receiveMetricsSuccess', updatedMetric); }) - .catch(error => { + .catch((error) => { dispatch('receiveMetricsError', error); createFlash(error); }); diff --git a/app/assets/javascripts/serverless/store/getters.js b/app/assets/javascripts/serverless/store/getters.js index c9b1d22799a..da975c56e5d 100644 --- a/app/assets/javascripts/serverless/store/getters.js +++ b/app/assets/javascripts/serverless/store/getters.js @@ -1,7 +1,7 @@ import { translate } from '../utils'; -export const hasPrometheusMissingData = state => state.hasPrometheus && !state.hasPrometheusData; +export const hasPrometheusMissingData = (state) => state.hasPrometheus && !state.hasPrometheusData; // Convert the function list into a k/v grouping based on the environment scope -export const getFunctions = state => translate(state.functions); +export const getFunctions = (state) => translate(state.functions); diff --git a/app/assets/javascripts/serverless/utils.js b/app/assets/javascripts/serverless/utils.js index 1bf03ea8d42..e218a9aa3fd 100644 --- a/app/assets/javascripts/serverless/utils.js +++ b/app/assets/javascripts/serverless/utils.js @@ -1,16 +1,16 @@ // Validate that the object coming in has valid query details and results -export const validateGraphData = data => +export const validateGraphData = (data) => data.queries && Array.isArray(data.queries) && - data.queries.filter(query => { + data.queries.filter((query) => { if (Array.isArray(query.result)) { - return query.result.filter(res => Array.isArray(res.values)).length === query.result.length; + return query.result.filter((res) => Array.isArray(res.values)).length === query.result.length; } return false; }).length === data.queries.length; -export const translate = functions => +export const translate = (functions) => functions.reduce( (acc, func) => Object.assign(acc, { diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue index f2685dfbcdb..c8efbd73b48 100644 --- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue +++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue @@ -6,7 +6,7 @@ import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete'; import { GlToast, GlModal, GlTooltipDirective, GlIcon, GlFormCheckbox } from '@gitlab/ui'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { __, s__ } from '~/locale'; -import Api from '~/api'; +import { updateUserStatus } from '~/rest_api'; import EmojiMenuInModal from './emoji_menu_in_modal'; import { isUserBusy, isValidAvailibility } from './utils'; import * as Emoji from '~/emoji'; @@ -163,7 +163,7 @@ export default { setStatus() { const { emoji, message, availability } = this; - Api.postUserStatus({ + updateUserStatus({ emoji, message, availability: availability ? AVAILABILITY_STATUS.BUSY : AVAILABILITY_STATUS.NOT_SET, @@ -172,10 +172,7 @@ export default { .catch(this.onUpdateFail); }, onUpdateSuccess() { - this.$toast.show(s__('SetStatusModal|Status updated'), { - type: 'success', - position: 'top-center', - }); + this.$toast.show(s__('SetStatusModal|Status updated')); this.closeModal(); window.location.reload(); }, diff --git a/app/assets/javascripts/set_status_modal/utils.js b/app/assets/javascripts/set_status_modal/utils.js index dccb66be11f..faee4012ef4 100644 --- a/app/assets/javascripts/set_status_modal/utils.js +++ b/app/assets/javascripts/set_status_modal/utils.js @@ -3,7 +3,7 @@ export const AVAILABILITY_STATUS = { NOT_SET: 'not_set', }; -export const isUserBusy = status => status === AVAILABILITY_STATUS.BUSY; +export const isUserBusy = (status) => status === AVAILABILITY_STATUS.BUSY; -export const isValidAvailibility = availability => +export const isValidAvailibility = (availability) => availability.length ? Object.values(AVAILABILITY_STATUS).includes(availability) : true; diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js index 18160421136..1f1f6e42576 100644 --- a/app/assets/javascripts/settings_panels.js +++ b/app/assets/javascripts/settings_panels.js @@ -1,13 +1,10 @@ import $ from 'jquery'; import { __ } from './locale'; -function expandSection($section) { +export function expandSection($section) { $section.find('.js-settings-toggle:not(.js-settings-toggle-trigger-only)').text(__('Collapse')); // eslint-disable-next-line @gitlab/no-global-event-off - $section - .find('.settings-content') - .off('scroll.expandSection') - .scrollTop(0); + $section.find('.settings-content').off('scroll.expandSection').scrollTop(0); $section.addClass('expanded'); if (!$section.hasClass('no-animate')) { $section @@ -16,7 +13,7 @@ function expandSection($section) { } } -function closeSection($section) { +export function closeSection($section) { $section.find('.js-settings-toggle:not(.js-settings-toggle-trigger-only)').text(__('Expand')); $section.find('.settings-content').on('scroll.expandSection', () => expandSection($section)); $section.removeClass('expanded'); @@ -27,7 +24,7 @@ function closeSection($section) { } } -function toggleSection($section) { +export function toggleSection($section) { $section.removeClass('no-animate'); if ($section.hasClass('expanded')) { closeSection($section); diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue index 3baf4bf0742..84e7110e2b2 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue @@ -34,8 +34,8 @@ export default { return !this.users.length; }, sortedAssigness() { - const canMergeUsers = this.users.filter(user => user.can_merge); - const canNotMergeUsers = this.users.filter(user => !user.can_merge); + const canMergeUsers = this.users.filter((user) => user.can_merge); + const canNotMergeUsers = this.users.filter((user) => !user.can_merge); return [...canMergeUsers, ...canNotMergeUsers]; }, diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue index bf0c52b2341..0eee287e0c2 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue @@ -59,7 +59,7 @@ export default { handleFetchResult({ data }) { const { nodes } = data.project.issue.assignees; - const assignees = nodes.map(n => ({ + const assignees = nodes.map((n) => ({ ...n, avatar_url: n.avatarUrl, id: getIdFromGraphQLId(n.id), diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue index 362ca4ab917..b713b0f960c 100644 --- a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue +++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue @@ -39,7 +39,7 @@ export default { return this.users.length > 2; }, allAssigneesCanMerge() { - return this.users.every(user => user.can_merge); + return this.users.every((user) => user.can_merge); }, sidebarAvatarCounter() { if (this.users.length > DEFAULT_MAX_COUNTER) { @@ -58,7 +58,7 @@ export default { return ''; } - const mergeLength = this.users.filter(u => u.can_merge).length; + const mergeLength = this.users.filter((u) => u.can_merge).length; if (mergeLength === this.users.length) { return ''; @@ -74,7 +74,7 @@ export default { tooltipTitle() { const maxRender = Math.min(DEFAULT_RENDER_COUNT, this.users.length); const renderUsers = this.users.slice(0, maxRender); - const names = renderUsers.map(u => u.name); + const names = renderUsers.map((u) => u.name); if (!this.users.length) { return __('Assignee(s)'); diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue index 46d51138ccf..d210f9efcb3 100644 --- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue +++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue @@ -48,7 +48,7 @@ export default { .then(() => { eventHub.$emit('updateIssuableConfidentiality', confidential); }) - .catch(err => { + .catch((err) => { Flash( err || __('Something went wrong trying to change the confidentiality of this issue'), ); diff --git a/app/assets/javascripts/sidebar/components/copy_email_to_clipboard.vue b/app/assets/javascripts/sidebar/components/copy_email_to_clipboard.vue new file mode 100644 index 00000000000..8c8241cf6a4 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/copy_email_to_clipboard.vue @@ -0,0 +1,43 @@ +<script> +import { s__, __, sprintf } from '~/locale'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; + +export default { + i18n: { + copyEmail: __('Copy email address'), + }, + components: { + ClipboardButton, + }, + props: { + copyText: { + type: String, + required: true, + }, + }, + computed: { + emailText() { + return sprintf(s__('RightSidebar|Issue email: %{copyText}'), { copyText: this.copyText }); + }, + }, +}; +</script> + +<template> + <div + data-qa-selector="copy-forward-email" + class="copy-email-address gl-display-flex gl-align-items-center gl-justify-content-space-between" + > + <span + class="gl-overflow-hidden gl-text-overflow-ellipsis gl-white-space-nowrap hide-collapsed gl-w-85p" + >{{ emailText }}</span + > + <clipboard-button + class="copy-email-button gl-bg-none!" + category="tertiary" + :title="$options.i18n.copyEmail" + :text="copyText" + tooltip-placement="left" + /> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue index 07abfa8d103..e01e1f032e3 100644 --- a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue +++ b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue @@ -50,9 +50,13 @@ export default { $(this.$el).trigger('hidden.gl.dropdown'); }, getUpdateVariables(dropdownLabels) { - const currentLabelIds = this.selectedLabels.map(label => label.id); - const userAddedLabelIds = dropdownLabels.filter(label => label.set).map(label => label.id); - const userRemovedLabelIds = dropdownLabels.filter(label => !label.set).map(label => label.id); + const currentLabelIds = this.selectedLabels.map((label) => label.id); + const userAddedLabelIds = dropdownLabels + .filter((label) => label.set) + .map((label) => label.id); + const userRemovedLabelIds = dropdownLabels + .filter((label) => !label.set) + .map((label) => label.id); const labelIds = difference(union(currentLabelIds, userAddedLabelIds), userRemovedLabelIds); @@ -116,7 +120,7 @@ export default { } const issuableType = camelCase(this.issuableType); - this.selectedLabels = data[mutationName]?.[issuableType]?.labels?.nodes?.map(label => ({ + this.selectedLabels = data[mutationName]?.[issuableType]?.labels?.nodes?.map((label) => ({ ...label, id: getIdFromGraphQLId(label.id), })); diff --git a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue index 10b16a44261..9554a98121f 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue @@ -33,7 +33,7 @@ export default { return this.users.length > 2; }, allReviewersCanMerge() { - return this.users.every(user => user.can_merge); + return this.users.every((user) => user.can_merge); }, sidebarAvatarCounter() { if (this.users.length > DEFAULT_MAX_COUNTER) { @@ -48,7 +48,7 @@ export default { return this.users.slice(0, collapsedLength); }, tooltipTitleMergeStatus() { - const mergeLength = this.users.filter(u => u.can_merge).length; + const mergeLength = this.users.filter((u) => u.can_merge).length; if (mergeLength === this.users.length) { return ''; @@ -64,7 +64,7 @@ export default { tooltipTitle() { const maxRender = Math.min(DEFAULT_RENDER_COUNT, this.users.length); const renderUsers = this.users.slice(0, maxRender); - const names = renderUsers.map(u => u.name); + const names = renderUsers.map((u) => u.name); if (!this.users.length) { return __('Reviewer(s)'); diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue index d64b483acb1..a461d992222 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue @@ -1,14 +1,13 @@ <script> // NOTE! For the first iteration, we are simply copying the implementation of Assignees // It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736 -import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; +import { GlLoadingIcon } from '@gitlab/ui'; import { n__ } from '~/locale'; export default { name: 'ReviewerTitle', components: { GlLoadingIcon, - GlIcon, }, props: { loading: { @@ -24,11 +23,6 @@ export default { type: Boolean, required: true, }, - showToggle: { - type: Boolean, - required: false, - default: false, - }, }, computed: { reviewerTitle() { @@ -52,14 +46,5 @@ export default { > {{ __('Edit') }} </a> - <a - v-if="showToggle" - :aria-label="__('Toggle sidebar')" - class="gutter-toggle float-right js-sidebar-toggle" - href="#" - role="button" - > - <gl-icon data-hidden="true" name="chevron-double-lg-right" :size="12" /> - </a> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue index 6a3d88f6385..cd62fe5be0f 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue @@ -36,8 +36,8 @@ export default { return !this.users.length; }, sortedReviewers() { - const canMergeUsers = this.users.filter(user => user.can_merge); - const canNotMergeUsers = this.users.filter(user => !user.can_merge); + const canMergeUsers = this.users.filter((user) => user.can_merge); + const canNotMergeUsers = this.users.filter((user) => !user.can_merge); return [...canMergeUsers, ...canNotMergeUsers]; }, diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue index aee94a55134..1a2473e5f6c 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue @@ -2,6 +2,7 @@ // NOTE! For the first iteration, we are simply copying the implementation of Assignees // It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736 import { deprecatedCreateFlash as Flash } from '~/flash'; +import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import eventHub from '~/sidebar/event_hub'; import Store from '~/sidebar/stores/sidebar_store'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -25,11 +26,6 @@ export default { type: String, required: true, }, - signedIn: { - type: Boolean, - required: false, - default: false, - }, issuableType: { type: String, required: false, @@ -80,8 +76,7 @@ export default { .saveReviewers(this.field) .then(() => { this.loading = false; - // Uncomment once this issue has been addressed > https://gitlab.com/gitlab-org/gitlab/-/issues/237922 - // refreshUserMergeRequestCounts(); + refreshUserMergeRequestCounts(); }) .catch(() => { this.loading = false; @@ -98,7 +93,6 @@ export default { :number-of-reviewers="store.reviewers.length" :loading="loading || store.isFetching.reviewers" :editable="store.editable" - :show-toggle="!signedIn" /> <reviewers v-if="!store.isFetching.reviewers" diff --git a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue index 8f3610b912a..0cf11e83349 100644 --- a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue +++ b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue @@ -41,7 +41,7 @@ export default { type: String, required: false, default: ISSUABLE_TYPES.INCIDENT, - validator: value => { + validator: (value) => { // currently severity is supported only for incidents, but this list might be extended return [ISSUABLE_TYPES.INCIDENT].includes(value); }, @@ -67,7 +67,7 @@ export default { return this.isDropdownShowing ? 'show' : 'gl-display-none'; }, selectedItem() { - return this.severitiesList.find(severity => severity.value === this.severity); + return this.severitiesList.find((severity) => severity.value === this.severity); }, }, mounted() { @@ -106,7 +106,7 @@ export default { projectPath: this.projectPath, }, }) - .then(resp => { + .then((resp) => { const { data: { issueSetSeverity: { diff --git a/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.vue deleted file mode 100644 index 8a80b1bf13f..00000000000 --- a/app/assets/javascripts/sidebar/components/time_tracking/estimate_only_pane.vue +++ /dev/null @@ -1,18 +0,0 @@ -<script> -export default { - name: 'TimeTrackingEstimateOnlyPane', - props: { - timeEstimateHumanReadable: { - type: String, - required: true, - }, - }, -}; -</script> - -<template> - <div data-testid="estimateOnlyPane"> - <span class="gl-font-weight-bold">{{ s__('TimeTracking|Estimated:') }} </span - >{{ timeEstimateHumanReadable }} - </div> -</template> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.vue deleted file mode 100644 index 2d3d0ce8dc5..00000000000 --- a/app/assets/javascripts/sidebar/components/time_tracking/no_tracking_pane.vue +++ /dev/null @@ -1,11 +0,0 @@ -<script> -export default { - name: 'TimeTrackingNoTrackingPane', -}; -</script> - -<template> - <div data-testid="noTrackingPane"> - <span class="no-value">{{ __('No estimate or time spent') }}</span> - </div> -</template> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue index 6bef5ed67a4..26e0a0da860 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue @@ -27,7 +27,7 @@ export default { listenForQuickActions() { $(document).on('ajax:success', '.gfm-form', this.quickActionListened); - eventHub.$on('timeTrackingUpdated', data => { + eventHub.$on('timeTrackingUpdated', (data) => { this.quickActionListened({ detail: [data] }); }); }, diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue index 3199ed1e615..26b8e087512 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue @@ -1,22 +1,23 @@ <script> import { GlIcon } from '@gitlab/ui'; +import { s__, __ } from '~/locale'; import TimeTrackingHelpState from './help_state.vue'; import TimeTrackingCollapsedState from './collapsed_state.vue'; import TimeTrackingSpentOnlyPane from './spent_only_pane.vue'; -import TimeTrackingNoTrackingPane from './no_tracking_pane.vue'; -import TimeTrackingEstimateOnlyPane from './estimate_only_pane.vue'; import TimeTrackingComparisonPane from './comparison_pane.vue'; import eventHub from '../../event_hub'; export default { name: 'IssuableTimeTracker', + i18n: { + noTimeTrackingText: __('No estimate or time spent'), + estimatedOnlyText: s__('TimeTracking|Estimated:'), + }, components: { GlIcon, TimeTrackingCollapsedState, - TimeTrackingEstimateOnlyPane, TimeTrackingSpentOnlyPane, - TimeTrackingNoTrackingPane, TimeTrackingComparisonPane, TimeTrackingHelpState, }, @@ -139,15 +140,17 @@ export default { </div> </div> <div class="time-tracking-content hide-collapsed"> - <time-tracking-estimate-only-pane - v-if="showEstimateOnlyState" - :time-estimate-human-readable="humanTimeEstimate" - /> + <div v-if="showEstimateOnlyState" data-testid="estimateOnlyPane"> + <span class="gl-font-weight-bold">{{ $options.i18n.estimatedOnlyText }} </span + >{{ humanTimeEstimate }} + </div> <time-tracking-spent-only-pane v-if="showSpentOnlyState" :time-spent-human-readable="humanTimeSpent" /> - <time-tracking-no-tracking-pane v-if="showNoTimeTrackingState" /> + <div v-if="showNoTimeTrackingState" data-testid="noTrackingPane"> + <span class="gl-text-gray-500">{{ $options.i18n.noTimeTrackingText }}</span> + </div> <time-tracking-comparison-pane v-if="showComparisonState" :time-estimate="timeEstimate" diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js index edeb1bba020..55847fc43f0 100644 --- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js +++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js @@ -46,14 +46,14 @@ class SidebarMoveIssue { () => new window.Flash(__('An error occurred while fetching projects autocomplete.')), ); }, - renderRow: project => ` + renderRow: (project) => ` <li> <a href="#" class="js-move-issue-dropdown-item"> ${escape(project.name_with_namespace)} </a> </li> `, - clicked: options => { + clicked: (options) => { const project = options.selectedObj; const selectedProjectId = options.isMarking ? project.id : 0; this.mediator.setMoveToProjectId(selectedProjectId); diff --git a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js index 0f5f8f2b53b..4d9e99941d1 100644 --- a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js @@ -16,7 +16,7 @@ export default class SidebarMilestone { components: { timeTracker, }, - render: createElement => + render: (createElement) => createElement('timeTracker', { props: { timeEstimate: parseInt(timeEstimate, 10), diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 984cd8a3b1d..2760bf431ea 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -12,6 +12,7 @@ import sidebarParticipants from './components/participants/sidebar_participants. import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue'; import SidebarSeverity from './components/severity/sidebar_severity.vue'; import Translate from '../vue_shared/translate'; +import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue'; import createDefaultClient from '~/lib/graphql'; import { isInIssuePage, isInIncidentPage, parseBoolean } from '~/lib/utils/common_utils'; import createFlash from '~/flash'; @@ -40,7 +41,7 @@ function mountAssigneesComponent(mediator) { components: { SidebarAssignees, }, - render: createElement => + render: (createElement) => createElement('sidebar-assignees', { props: { mediator, @@ -70,14 +71,13 @@ function mountReviewersComponent(mediator) { components: { SidebarReviewers, }, - render: createElement => + render: (createElement) => createElement('sidebar-reviewers', { props: { mediator, issuableIid: String(iid), projectPath: fullPath, field: el.dataset.field, - signedIn: el.hasAttribute('data-signed-in'), issuableType: isInIssuePage() ? 'issue' : 'merge_request', }, }), @@ -105,7 +105,7 @@ export function mountSidebarLabels() { allowScopedLabels: parseBoolean(el.dataset.allowScopedLabels), initiallySelectedLabels: JSON.parse(el.dataset.selectedLabels), }, - render: createElement => createElement(SidebarLabels), + render: (createElement) => createElement(SidebarLabels), }); } @@ -128,7 +128,7 @@ function mountConfidentialComponent(mediator) { components: { ConfidentialIssueSidebar, }, - render: createElement => + render: (createElement) => createElement('confidential-issue-sidebar', { props: { iid: String(iid), @@ -163,20 +163,20 @@ function mountLockComponent() { ); } else { importStore = import(/* webpackChunkName: 'mrNotesStore' */ '~/mr_notes/stores').then( - store => store.default, + (store) => store.default, ); } importStore .then( - store => + (store) => new Vue({ el, store, provide: { fullPath, }, - render: createElement => + render: (createElement) => createElement(IssuableLockForm, { props: { isEditable: initialData.is_editable, @@ -200,7 +200,7 @@ function mountParticipantsComponent(mediator) { components: { sidebarParticipants, }, - render: createElement => + render: (createElement) => createElement('sidebar-participants', { props: { mediator, @@ -220,7 +220,7 @@ function mountSubscriptionsComponent(mediator) { components: { sidebarSubscriptions, }, - render: createElement => + render: (createElement) => createElement('sidebar-subscriptions', { props: { mediator, @@ -240,7 +240,7 @@ function mountTimeTrackingComponent() { components: { SidebarTimeTracking, }, - render: createElement => createElement('sidebar-time-tracking', {}), + render: (createElement) => createElement('sidebar-time-tracking', {}), }); } @@ -262,7 +262,7 @@ function mountSeverityComponent() { components: { SidebarSeverity, }, - render: createElement => + render: (createElement) => createElement('sidebar-severity', { props: { projectPath: fullPath, @@ -273,6 +273,21 @@ function mountSeverityComponent() { }); } +function mountCopyEmailComponent() { + const el = document.getElementById('issuable-copy-email'); + + if (!el) return; + + const { createNoteEmail } = getSidebarOptions(); + + // eslint-disable-next-line no-new + new Vue({ + el, + render: (createElement) => + createElement(CopyEmailToClipboard, { props: { copyText: createNoteEmail } }), + }); +} + export function mountSidebar(mediator) { mountAssigneesComponent(mediator); mountReviewersComponent(mediator); @@ -280,6 +295,7 @@ export function mountSidebar(mediator) { mountLockComponent(); mountParticipantsComponent(mediator); mountSubscriptionsComponent(mediator); + mountCopyEmailComponent(); new SidebarMoveIssue( mediator, diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js index 2146fb83b13..d143283653b 100644 --- a/app/assets/javascripts/sidebar/sidebar_mediator.js +++ b/app/assets/javascripts/sidebar/sidebar_mediator.js @@ -30,7 +30,7 @@ export default class SidebarMediator { } saveAssignees(field) { - const selected = this.store.assignees.map(u => u.id); + const selected = this.store.assignees.map((u) => u.id); // If there are no ids, that means we have to unassign (which is id = 0) // And it only accepts an array, hence [0] @@ -41,7 +41,7 @@ export default class SidebarMediator { } saveReviewers(field) { - const selected = this.store.reviewers.map(u => u.id); + const selected = this.store.reviewers.map((u) => u.id); // If there are no ids, that means we have to unassign (which is id = 0) // And it only accepts an array, hence [0] @@ -80,7 +80,7 @@ export default class SidebarMediator { this.store.setSubscribedState(!this.store.subscribed); this.store.setFetchingState('subscriptions', false); }) - .catch(err => { + .catch((err) => { this.store.setFetchingState('subscriptions', false); throw err; }); diff --git a/app/assets/javascripts/sidebar/utils.js b/app/assets/javascripts/sidebar/utils.js index 23730508b56..20cd4ce9d99 100644 --- a/app/assets/javascripts/sidebar/utils.js +++ b/app/assets/javascripts/sidebar/utils.js @@ -1 +1 @@ -export const toLabelGid = id => `gid://gitlab/Label/${id}`; +export const toLabelGid = (id) => `gid://gitlab/Label/${id}`; diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index f751df6367e..192eb0784d4 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -29,22 +29,17 @@ export default class SingleFileDiff { this.isOpen = !this.diffForPath; if (this.diffForPath) { this.collapsedContent = this.content; - this.loadingContent = $(WRAPPER) - .addClass('loading') - .html(LOADING_HTML) - .hide(); + this.loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide(); this.content = null; this.collapsedContent.after(this.loadingContent); this.$chevronRightIcon.removeClass('gl-display-none'); } else { - this.collapsedContent = $(WRAPPER) - .html(COLLAPSED_HTML) - .hide(); + this.collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide(); this.content.after(this.collapsedContent); this.$chevronDownIcon.removeClass('gl-display-none'); } - $('.js-file-title, .click-to-expand', this.file).on('click', e => { + $('.js-file-title, .click-to-expand', this.file).on('click', (e) => { this.toggleDiff($(e.target)); }); } diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js index c4655d35cf0..15d04dadb15 100644 --- a/app/assets/javascripts/smart_interval.js +++ b/app/assets/javascripts/smart_interval.js @@ -96,9 +96,7 @@ export default class SmartInterval { window.removeEventListener('focus', this.onWindowVisibilityChange); this.cancel(); // eslint-disable-next-line @gitlab/no-global-event-off - $(document) - .off('visibilitychange') - .off('beforeunload'); + $(document).off('visibilitychange').off('beforeunload'); } /* private */ @@ -121,7 +119,7 @@ export default class SmartInterval { .then(() => { this.isLoading = false; }) - .catch(err => { + .catch((err) => { this.isLoading = false; throw err; }); diff --git a/app/assets/javascripts/snippet/collapsible_input.js b/app/assets/javascripts/snippet/collapsible_input.js index e7225162f86..75465d96043 100644 --- a/app/assets/javascripts/snippet/collapsible_input.js +++ b/app/assets/javascripts/snippet/collapsible_input.js @@ -1,7 +1,7 @@ -const hide = el => el.classList.add('d-none'); -const show = el => el.classList.remove('d-none'); +const hide = (el) => el.classList.add('d-none'); +const show = (el) => el.classList.remove('d-none'); -const setupCollapsibleInput = el => { +const setupCollapsibleInput = (el) => { const collapsedEl = el.querySelector('.js-collapsed'); const expandedEl = el.querySelector('.js-expanded'); const collapsedInputEl = collapsedEl.querySelector('textarea,input,select'); @@ -21,7 +21,7 @@ const setupCollapsibleInput = el => { // NOTE: // We add focus listener to all form inputs so that we can collapse // when something is focused that's not the expanded input. - formEl.addEventListener('focusin', e => { + formEl.addEventListener('focusin', (e) => { if (e.target === collapsedInputEl) { expand(); expandedInputEl.focus(); diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue index 08683f25651..ffb5e242973 100644 --- a/app/assets/javascripts/snippets/components/edit.vue +++ b/app/assets/javascripts/snippets/components/edit.vue @@ -73,7 +73,7 @@ export default { return this.actions.length > 0; }, hasValidBlobs() { - return this.actions.every(x => x.content); + return this.actions.every((x) => x.content); }, updatePrevented() { return this.snippet.title === '' || !this.hasValidBlobs || this.isUpdating; @@ -130,7 +130,7 @@ export default { }, getAttachedFiles() { const fileInputs = Array.from(this.$el.querySelectorAll('[name="files[]"]')); - return fileInputs.map(node => node.value); + return fileInputs.map((node) => node.value); }, createMutation() { return { @@ -166,7 +166,7 @@ export default { redirectTo(baseObj.snippet.webUrl); } }) - .catch(e => { + .catch((e) => { this.flashAPIFailure(e); }); }, diff --git a/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue index ab2553265a2..ff27c90a84d 100644 --- a/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue +++ b/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue @@ -74,7 +74,7 @@ export default { this.blobsOrig = blobsById; this.blobs = cloneDeep(blobsById); - this.blobIds = blobs.map(x => x.id); + this.blobIds = blobs.map((x) => x.id); // Show 1 empty blob if none exist if (!this.blobIds.length) { @@ -108,7 +108,7 @@ export default { this.blobIds.push(blob.id); }, deleteBlob(id) { - this.blobIds = this.blobIds.filter(x => x !== id); + this.blobIds = this.blobIds.filter((x) => x !== id); this.$delete(this.blobs, id); }, updateBlob(id, args) { diff --git a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue index 6a10dc38f2c..c8545e334a6 100644 --- a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue +++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue @@ -55,12 +55,12 @@ export default { axios .get(url, { // This prevents axios from automatically JSON.parse response - transformResponse: [f => f], + transformResponse: [(f) => f], }) - .then(res => { + .then((res) => { this.notifyAboutUpdates({ content: res.data }); }) - .catch(e => this.flashAPIFailure(e)); + .catch((e) => this.flashAPIFailure(e)); }, flashAPIFailure(err) { Flash(sprintf(SNIPPET_BLOB_CONTENT_FETCH_ERROR, { err })); diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue index b965c15306d..4326c3c3159 100644 --- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue +++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue @@ -41,6 +41,11 @@ export default { }, }, }, + provide() { + return { + blobHash: Math.random().toString().split('.')[1], + }; + }, props: { snippet: { type: Object, @@ -51,13 +56,6 @@ export default { required: true, }, }, - provide() { - return { - blobHash: Math.random() - .toString() - .split('.')[1], - }; - }, data() { return { blobContent: '', @@ -90,7 +88,7 @@ export default { const { blobs: { nodes: dataBlobs }, } = data.snippets.nodes[0]; - const updatedBlobData = dataBlobs.find(blob => blob.path === blobPath); + const updatedBlobData = dataBlobs.find((blob) => blob.path === blobPath); return updatedBlobData.richData || updatedBlobData.plainData; }, }, diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue index 32c4c1039f5..5ba62908b43 100644 --- a/app/assets/javascripts/snippets/components/snippet_header.vue +++ b/app/assets/javascripts/snippets/components/snippet_header.vue @@ -69,7 +69,7 @@ export default { }, computed: { snippetHasBinary() { - return Boolean(this.snippet.blobs.find(blob => blob.binary)); + return Boolean(this.snippet.blobs.find((blob) => blob.binary)); }, authoredMessage() { return this.snippet.author @@ -164,7 +164,7 @@ export default { this.closeDeleteModal(); this.redirectToSnippets(); }) - .catch(err => { + .catch((err) => { this.isDeleting = false; this.errorMessage = err.message; }); @@ -200,6 +200,13 @@ export default { <gl-avatar :size="24" :src="snippet.author.avatarUrl" /> <span class="bold">{{ snippet.author.name }}</span> </a> + <gl-emoji + v-if="snippet.author.status" + v-gl-tooltip + class="gl-vertical-align-baseline font-size-inherit gl-mr-1" + :title="snippet.author.status.message" + :data-name="snippet.author.status.emoji" + /> </template> </gl-sprintf> </div> diff --git a/app/assets/javascripts/snippets/mixins/snippets.js b/app/assets/javascripts/snippets/mixins/snippets.js index 5844a55e4f5..89a88958152 100644 --- a/app/assets/javascripts/snippets/mixins/snippets.js +++ b/app/assets/javascripts/snippets/mixins/snippets.js @@ -11,7 +11,7 @@ export const getSnippetMixin = { ids: [this.snippetGid], }; }, - update: data => { + update: (data) => { const res = data.snippets.nodes[0]; if (res) { res.blobs = res.blobs.nodes; diff --git a/app/assets/javascripts/snippets/utils/blob.js b/app/assets/javascripts/snippets/utils/blob.js index 5081c648e36..a47418323f2 100644 --- a/app/assets/javascripts/snippets/utils/blob.js +++ b/app/assets/javascripts/snippets/utils/blob.js @@ -12,7 +12,7 @@ import { SNIPPET_MARK_BLOBS_CONTENT, SNIPPET_MEASURE_BLOBS_CONTENT } from '~/per const createLocalId = () => uniqueId('blob_local_'); -export const decorateBlob = blob => ({ +export const decorateBlob = (blob) => ({ ...blob, id: createLocalId(), isLoaded: false, @@ -54,7 +54,7 @@ const diff = ({ content, path }, origBlob) => { */ export const diffAll = (blobs, origBlobs) => { const deletedEntries = Object.values(origBlobs) - .filter(x => !blobs[x.id]) + .filter((x) => !blobs[x.id]) .map(({ path, content }) => ({ action: SNIPPET_BLOB_ACTION_DELETE, previousPath: path, @@ -63,15 +63,15 @@ export const diffAll = (blobs, origBlobs) => { })); const newEntries = Object.values(blobs) - .map(blob => diff(blob, origBlobs[blob.id])) - .filter(x => x); + .map((blob) => diff(blob, origBlobs[blob.id])) + .filter((x) => x); return [...deletedEntries, ...newEntries]; }; -export const defaultSnippetVisibilityLevels = arr => { +export const defaultSnippetVisibilityLevels = (arr) => { if (Array.isArray(arr)) { - return arr.map(l => { + return arr.map((l) => { const translatedLevel = SNIPPET_LEVELS_MAP[l]; return { value: translatedLevel, diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js index 64842ae7f8d..232c97ecae0 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -16,10 +16,7 @@ export default class Star { .post($this.data('endpoint')) .then(({ data }) => { const isStarred = $starSpan.hasClass('starred'); - $this - .parent() - .find('.count') - .text(data.star_count); + $this.parent().find('.count').text(data.star_count); if (isStarred) { $starSpan.removeClass('starred').text(s__('StarProject|Star')); diff --git a/app/assets/javascripts/static_site_editor/graphql/index.js b/app/assets/javascripts/static_site_editor/graphql/index.js index a13f7d3ad53..bce320ed805 100644 --- a/app/assets/javascripts/static_site_editor/graphql/index.js +++ b/app/assets/javascripts/static_site_editor/graphql/index.js @@ -8,7 +8,7 @@ import hasSubmittedChangesResolver from './resolvers/has_submitted_changes'; Vue.use(VueApollo); -const createApolloProvider = appData => { +const createApolloProvider = (appData) => { const defaultClient = createDefaultClient( { Project: { @@ -26,7 +26,7 @@ const createApolloProvider = appData => { ); // eslint-disable-next-line @gitlab/require-i18n-strings - const mounts = appData.mounts.map(mount => ({ __typename: 'Mount', ...mount })); + const mounts = appData.mounts.map((mount) => ({ __typename: 'Mount', ...mount })); defaultClient.cache.writeData({ data: { diff --git a/app/assets/javascripts/static_site_editor/graphql/resolvers/file.js b/app/assets/javascripts/static_site_editor/graphql/resolvers/file.js index 16f176581cb..fc3cac52e2a 100644 --- a/app/assets/javascripts/static_site_editor/graphql/resolvers/file.js +++ b/app/assets/javascripts/static_site_editor/graphql/resolvers/file.js @@ -1,7 +1,7 @@ import loadSourceContent from '../../services/load_source_content'; const fileResolver = ({ fullPath: projectId }, { path: sourcePath }) => { - return loadSourceContent({ projectId, sourcePath }).then(sourceContent => ({ + return loadSourceContent({ projectId, sourcePath }).then((sourceContent) => ({ // eslint-disable-next-line @gitlab/require-i18n-strings __typename: 'File', ...sourceContent, diff --git a/app/assets/javascripts/static_site_editor/graphql/resolvers/has_submitted_changes.js b/app/assets/javascripts/static_site_editor/graphql/resolvers/has_submitted_changes.js index ea49b21eb0d..35ecf6d698c 100644 --- a/app/assets/javascripts/static_site_editor/graphql/resolvers/has_submitted_changes.js +++ b/app/assets/javascripts/static_site_editor/graphql/resolvers/has_submitted_changes.js @@ -4,7 +4,7 @@ import query from '../queries/app_data.query.graphql'; const hasSubmittedChangesResolver = (_, { input: { hasSubmittedChanges } }, { cache }) => { const oldData = cache.readQuery({ query }); - const data = produce(oldData, draftState => { + const data = produce(oldData, (draftState) => { // punctually modifying draftState as per immer docs upsets our linters return { ...draftState, diff --git a/app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js b/app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js index 1bd79d40071..0b74c99b319 100644 --- a/app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js +++ b/app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js @@ -25,8 +25,8 @@ const submitContentChangesResolver = ( images, mergeRequestMeta, formattedMarkdown, - }).then(savedContentMeta => { - const data = produce(savedContentMeta, draftState => { + }).then((savedContentMeta) => { + const data = produce(savedContentMeta, (draftState) => { return { savedContentMeta: { __typename: 'SavedContentMeta', diff --git a/app/assets/javascripts/static_site_editor/image_repository.js b/app/assets/javascripts/static_site_editor/image_repository.js index b5ff4385d3c..56b2434d2e2 100644 --- a/app/assets/javascripts/static_site_editor/image_repository.js +++ b/app/assets/javascripts/static_site_editor/image_repository.js @@ -4,15 +4,15 @@ import { getBinary } from './services/image_service'; const imageRepository = () => { const images = new Map(); - const flash = message => new Flash(message); + const flash = (message) => new Flash(message); const add = (file, url) => { getBinary(file) - .then(content => images.set(url, content)) + .then((content) => images.set(url, content)) .catch(() => flash(__('Something went wrong while inserting your image. Please try again.'))); }; - const get = path => images.get(path); + const get = (path) => images.get(path); const getAll = () => images; diff --git a/app/assets/javascripts/static_site_editor/index.js b/app/assets/javascripts/static_site_editor/index.js index b58564388de..fbb14be21ba 100644 --- a/app/assets/javascripts/static_site_editor/index.js +++ b/app/assets/javascripts/static_site_editor/index.js @@ -4,7 +4,7 @@ import App from './components/app.vue'; import createRouter from './router'; import createApolloProvider from './graphql'; -const initStaticSiteEditor = el => { +const initStaticSiteEditor = (el) => { const { isSupportedContent, path: sourcePath, diff --git a/app/assets/javascripts/static_site_editor/pages/home.vue b/app/assets/javascripts/static_site_editor/pages/home.vue index 1e52e73294e..6c958cb2d22 100644 --- a/app/assets/javascripts/static_site_editor/pages/home.vue +++ b/app/assets/javascripts/static_site_editor/pages/home.vue @@ -118,7 +118,7 @@ export default { }, }, }) - .catch(e => { + .catch((e) => { this.submitChangesError = e.message; }) .finally(() => { diff --git a/app/assets/javascripts/static_site_editor/services/formatter.js b/app/assets/javascripts/static_site_editor/services/formatter.js index 9a5dcd307eb..e841c664406 100644 --- a/app/assets/javascripts/static_site_editor/services/formatter.js +++ b/app/assets/javascripts/static_site_editor/services/formatter.js @@ -24,7 +24,7 @@ const nestedLineRegexp = /^\s+/; * This function attempts to correct this problem before the content is loaded * by Toast UI. */ -const correctNestedContentIndenation = source => { +const correctNestedContentIndenation = (source) => { const lines = source.split('\n'); let topLevelOrderedListDetected = false; @@ -40,7 +40,7 @@ const correctNestedContentIndenation = source => { .join('\n'); }; -const removeOrphanedBrTags = source => { +const removeOrphanedBrTags = (source) => { /* Until the underlying Squire editor of Toast UI Editor resolves duplicate `<br>` tags, this `replace` solution will clear out orphaned `<br>` tags that it generates. Additionally, it cleans up orphaned `<br>` tags in the source markdown document that should be new lines. @@ -49,7 +49,7 @@ const removeOrphanedBrTags = source => { return source.replace(/\n^<br>$/gm, ''); }; -const format = source => { +const format = (source) => { return correctNestedContentIndenation(removeOrphanedBrTags(source)); }; diff --git a/app/assets/javascripts/static_site_editor/services/front_matterify.js b/app/assets/javascripts/static_site_editor/services/front_matterify.js index 60a5d799d11..6b897b42648 100644 --- a/app/assets/javascripts/static_site_editor/services/front_matterify.js +++ b/app/assets/javascripts/static_site_editor/services/front_matterify.js @@ -8,7 +8,7 @@ const hasMatter = (firstThreeChars, fourthChar) => { return isYamlDelimiter && isFourthCharNewline; }; -export const frontMatterify = source => { +export const frontMatterify = (source) => { let index = 3; let offset; const delimiter = source.slice(0, index); diff --git a/app/assets/javascripts/static_site_editor/services/image_service.js b/app/assets/javascripts/static_site_editor/services/image_service.js index 25ab1084572..a9b85057e3d 100644 --- a/app/assets/javascripts/static_site_editor/services/image_service.js +++ b/app/assets/javascripts/static_site_editor/services/image_service.js @@ -1,8 +1,8 @@ -export const getBinary = file => { +export const getBinary = (file) => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => resolve(reader.result.split(',')[1]); - reader.onerror = error => reject(error); + reader.onerror = (error) => reject(error); }); }; diff --git a/app/assets/javascripts/static_site_editor/services/load_source_content.js b/app/assets/javascripts/static_site_editor/services/load_source_content.js index 1af93a8a2bc..fcf69efafd8 100644 --- a/app/assets/javascripts/static_site_editor/services/load_source_content.js +++ b/app/assets/javascripts/static_site_editor/services/load_source_content.js @@ -1,6 +1,6 @@ import Api from '~/api'; -const extractTitle = content => { +const extractTitle = (content) => { const matches = content.match(/title: (.+)\n/i); return matches ? Array.from(matches)[1] : ''; diff --git a/app/assets/javascripts/static_site_editor/services/parse_source_file.js b/app/assets/javascripts/static_site_editor/services/parse_source_file.js index 39126eb7bcc..d7499d75a21 100644 --- a/app/assets/javascripts/static_site_editor/services/parse_source_file.js +++ b/app/assets/javascripts/static_site_editor/services/parse_source_file.js @@ -1,6 +1,6 @@ import { frontMatterify, stringify } from './front_matterify'; -const parseSourceFile = raw => { +const parseSourceFile = (raw) => { let editable; const syncContent = (newVal, isBody) => { @@ -20,7 +20,7 @@ const parseSourceFile = raw => { const matter = () => editable.matter; - const syncMatter = settings => { + const syncMatter = (settings) => { editable.matter = settings; }; diff --git a/app/assets/javascripts/static_site_editor/services/renderers/render_image.js b/app/assets/javascripts/static_site_editor/services/renderers/render_image.js index b0d863bdb5a..b5651e7163e 100644 --- a/app/assets/javascripts/static_site_editor/services/renderers/render_image.js +++ b/app/assets/javascripts/static_site_editor/services/renderers/render_image.js @@ -4,16 +4,16 @@ const canRender = ({ type }) => type === 'image'; let metadata; -const getCachedContent = basePath => metadata.imageRepository.get(basePath); +const getCachedContent = (basePath) => metadata.imageRepository.get(basePath); -const isRelativeToCurrentDirectory = basePath => !basePath.startsWith('/'); +const isRelativeToCurrentDirectory = (basePath) => !basePath.startsWith('/'); -const extractSourceDirectory = url => { +const extractSourceDirectory = (url) => { const sourceDir = /^(.+)\/([^/]+)$/.exec(url); // Extracts the base path and fileName from an image path return sourceDir || [null, null, url]; // If no source directory was extracted it means only a fileName was specified (e.g. url='file.png') }; -const parseCurrentDirectory = basePath => { +const parseCurrentDirectory = (basePath) => { const baseUrl = decodeURIComponent(metadata.baseUrl); const sourceDirectory = extractSourceDirectory(baseUrl)[1]; const currentDirectory = sourceDirectory.split(`/-/sse/${metadata.branch}`)[1]; @@ -23,7 +23,7 @@ const parseCurrentDirectory = basePath => { // For more context around this logic, please see the following comment: // https://gitlab.com/gitlab-org/gitlab/-/issues/241166#note_409413500 -const generateSourceDirectory = basePath => { +const generateSourceDirectory = (basePath) => { let sourceDir = ''; let defaultSourceDir = ''; diff --git a/app/assets/javascripts/static_site_editor/services/submit_content_changes.js b/app/assets/javascripts/static_site_editor/services/submit_content_changes.js index e57028ea05a..84e90deacfc 100644 --- a/app/assets/javascripts/static_site_editor/services/submit_content_changes.js +++ b/app/assets/javascripts/static_site_editor/services/submit_content_changes.js @@ -32,7 +32,7 @@ const createImageActions = (images, markdown) => { } images.forEach((imageContent, filePath) => { - const imageExistsInMarkdown = path => new RegExp(`!\\[([^[\\]\\n]*)\\](\\(${path})\\)`); // matches the image markdown syntax: ![<any-string-except-newline>](<path>) + const imageExistsInMarkdown = (path) => new RegExp(`!\\[([^[\\]\\n]*)\\](\\(${path})\\)`); // matches the image markdown syntax: ![<any-string-except-newline>](<path>) if (imageExistsInMarkdown(filePath).test(markdown)) { actions.push( diff --git a/app/assets/javascripts/static_site_editor/services/templater.js b/app/assets/javascripts/static_site_editor/services/templater.js index d302aea78a3..47fc36c3d18 100644 --- a/app/assets/javascripts/static_site_editor/services/templater.js +++ b/app/assets/javascripts/static_site_editor/services/templater.js @@ -40,10 +40,10 @@ const mark = (source, groups) => { const hash = {}; Object.entries(groups).forEach(([groupKey, group]) => { - group.forEach(pattern => { + group.forEach((pattern) => { const matches = text.match(pattern); if (matches) { - matches.forEach(match => { + matches.forEach((match) => { const key = `${markPrefix}-${groupKey}-${id}`; text = text.replace(match, key); hash[key] = match; @@ -67,12 +67,12 @@ const unmark = (text, hash) => { return source; }; -const unwrap = source => { +const unwrap = (source) => { let text = source; const matches = text.match(reTemplated); if (matches) { - matches.forEach(match => { + matches.forEach((match) => { const initial = match.replace(`${wrapPrefix}`, '').replace(`${wrapPostfix}`, ''); text = text.replace(match, initial); }); @@ -81,7 +81,7 @@ const unwrap = source => { return text; }; -const wrap = source => { +const wrap = (source) => { const { text, hash } = mark(unwrap(source), patternGroups); return unmark(text, hash); }; diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js index b51951674d5..81d9d9d37a7 100644 --- a/app/assets/javascripts/task_list.js +++ b/app/assets/javascripts/task_list.js @@ -35,7 +35,7 @@ export default class TaskList { `${this.taskListContainerSelector} .js-task-list-field[data-value]`, ); - taskListFields.forEach(taskListField => { + taskListFields.forEach((taskListField) => { // eslint-disable-next-line no-param-reassign taskListField.value = taskListField.dataset.value; }); diff --git a/app/assets/javascripts/terminal/terminal.js b/app/assets/javascripts/terminal/terminal.js index bae320cb705..26cc538994f 100644 --- a/app/assets/javascripts/terminal/terminal.js +++ b/app/assets/javascripts/terminal/terminal.js @@ -64,11 +64,11 @@ export default class GLTerminal { const decoder = new TextDecoder('utf-8'); const encoder = new TextEncoder('utf-8'); - this.terminal.on('data', data => { + this.terminal.on('data', (data) => { this.socket.send(encoder.encode(data)); }); - this.socket.addEventListener('message', ev => { + this.socket.addEventListener('message', (ev) => { this.terminal.write(decoder.decode(ev.data)); }); @@ -110,7 +110,7 @@ export default class GLTerminal { this.terminal.dispose(); this.socket.close(); - this.onDispose.forEach(fn => fn()); + this.onDispose.forEach((fn) => fn()); this.onDispose.length = 0; } diff --git a/app/assets/javascripts/terraform/components/terraform_list.vue b/app/assets/javascripts/terraform/components/terraform_list.vue index 26a0bfe5fa5..b71133d8813 100644 --- a/app/assets/javascripts/terraform/components/terraform_list.vue +++ b/app/assets/javascripts/terraform/components/terraform_list.vue @@ -15,7 +15,7 @@ export default { ...this.cursor, }; }, - update: data => data, + update: (data) => data, error() { this.states = null; }, diff --git a/app/assets/javascripts/test_utils/simulate_input.js b/app/assets/javascripts/test_utils/simulate_input.js index c300c806e6d..90c1b7cb57e 100644 --- a/app/assets/javascripts/test_utils/simulate_input.js +++ b/app/assets/javascripts/test_utils/simulate_input.js @@ -12,7 +12,7 @@ export default function simulateInput(target, text) { } if (text.length > 0) { - Array.prototype.forEach.call(text, char => { + Array.prototype.forEach.call(text, (char) => { input.value += char; triggerEvents(input); }); diff --git a/app/assets/javascripts/toggle_buttons.js b/app/assets/javascripts/toggle_buttons.js index 10a9d29e694..2f933823842 100644 --- a/app/assets/javascripts/toggle_buttons.js +++ b/app/assets/javascripts/toggle_buttons.js @@ -49,7 +49,7 @@ function onToggleClicked(toggle, input, clickCallback) { export default function setupToggleButtons(container, clickCallback = () => {}) { const toggles = container.querySelectorAll('.js-project-feature-toggle'); - toggles.forEach(toggle => { + toggles.forEach((toggle) => { const input = toggle.querySelector('.js-project-feature-toggle-input'); const isOn = parseBoolean(input.value); diff --git a/app/assets/javascripts/tooltips/components/tooltips.vue b/app/assets/javascripts/tooltips/components/tooltips.vue index 05927006ea6..90bdf06bc4c 100644 --- a/app/assets/javascripts/tooltips/components/tooltips.vue +++ b/app/assets/javascripts/tooltips/components/tooltips.vue @@ -2,7 +2,7 @@ import { GlTooltip, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import { uniqueId } from 'lodash'; -const getTooltipTitle = element => { +const getTooltipTitle = (element) => { return element.getAttribute('title') || element.dataset.title; }; @@ -37,8 +37,8 @@ export default { }; }, created() { - this.observer = new MutationObserver(mutations => { - mutations.forEach(mutation => { + this.observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { mutation.removedNodes.forEach(this.dispose); }); }); @@ -49,10 +49,11 @@ export default { methods: { addTooltips(elements, config) { const newTooltips = elements - .filter(element => !this.tooltipExists(element)) - .map(element => newTooltip(element, config)); + .filter((element) => !this.tooltipExists(element)) + .map((element) => newTooltip(element, config)) + .filter((tooltip) => tooltip.title); - newTooltips.forEach(tooltip => this.observe(tooltip)); + newTooltips.forEach((tooltip) => this.observe(tooltip)); this.tooltips.push(...newTooltips); }, @@ -90,9 +91,12 @@ export default { return Boolean(this.findTooltipByTarget(element)); }, findTooltipByTarget(element) { - return this.tooltips.find(tooltip => tooltip.target === element); + return this.tooltips.find((tooltip) => tooltip.target === element); }, }, + safeHtmlConfig: { + ADD_TAGS: ['gl-emoji'], + }, }; </script> <template> @@ -110,7 +114,7 @@ export default { :disabled="tooltip.disabled" :show="tooltip.show" > - <span v-if="tooltip.html" v-safe-html="tooltip.title"></span> + <span v-if="tooltip.html" v-safe-html:[$options.safeHtmlConfig]="tooltip.title"></span> <span v-else>{{ tooltip.title }}</span> </gl-tooltip> </div> diff --git a/app/assets/javascripts/tooltips/index.js b/app/assets/javascripts/tooltips/index.js index f7cad6639ae..b216affc818 100644 --- a/app/assets/javascripts/tooltips/index.js +++ b/app/assets/javascripts/tooltips/index.js @@ -68,7 +68,7 @@ const invokeBootstrapApi = (elements, method) => { } }; -const isGlTooltipsEnabled = () => Boolean(window.gon.glTooltipsEnabled); +const isGlTooltipsEnabled = () => Boolean(window.gon.features?.glTooltips); const tooltipApiInvoker = ({ glHandler, bsHandler }) => (elements, ...params) => { if (isGlTooltipsEnabled()) { @@ -81,12 +81,12 @@ const tooltipApiInvoker = ({ glHandler, bsHandler }) => (elements, ...params) => export const initTooltips = (config = {}) => { if (isGlTooltipsEnabled()) { const triggers = config?.triggers || DEFAULT_TRIGGER; - const events = triggers.split(' ').map(trigger => EVENTS_MAP[trigger]); + const events = triggers.split(' ').map((trigger) => EVENTS_MAP[trigger]); - events.forEach(event => { + events.forEach((event) => { document.addEventListener( event, - e => handleTooltipEvent(document, e, config.selector, config), + (e) => handleTooltipEvent(document, e, config.selector, config), true, ); }); @@ -103,28 +103,28 @@ export const add = (elements, config = {}) => { return invokeBootstrapApi(elements, config); }; export const dispose = tooltipApiInvoker({ - glHandler: element => tooltipsApp().dispose(element), - bsHandler: elements => invokeBootstrapApi(elements, 'dispose'), + glHandler: (element) => tooltipsApp().dispose(element), + bsHandler: (elements) => invokeBootstrapApi(elements, 'dispose'), }); export const fixTitle = tooltipApiInvoker({ - glHandler: element => tooltipsApp().fixTitle(element), - bsHandler: elements => invokeBootstrapApi(elements, '_fixTitle'), + glHandler: (element) => tooltipsApp().fixTitle(element), + bsHandler: (elements) => invokeBootstrapApi(elements, '_fixTitle'), }); export const enable = tooltipApiInvoker({ - glHandler: element => tooltipsApp().triggerEvent(element, 'enable'), - bsHandler: elements => invokeBootstrapApi(elements, 'enable'), + glHandler: (element) => tooltipsApp().triggerEvent(element, 'enable'), + bsHandler: (elements) => invokeBootstrapApi(elements, 'enable'), }); export const disable = tooltipApiInvoker({ - glHandler: element => tooltipsApp().triggerEvent(element, 'disable'), - bsHandler: elements => invokeBootstrapApi(elements, 'disable'), + glHandler: (element) => tooltipsApp().triggerEvent(element, 'disable'), + bsHandler: (elements) => invokeBootstrapApi(elements, 'disable'), }); export const hide = tooltipApiInvoker({ - glHandler: element => tooltipsApp().triggerEvent(element, 'close'), - bsHandler: elements => invokeBootstrapApi(elements, 'hide'), + glHandler: (element) => tooltipsApp().triggerEvent(element, 'close'), + bsHandler: (elements) => invokeBootstrapApi(elements, 'hide'), }); export const show = tooltipApiInvoker({ - glHandler: element => tooltipsApp().triggerEvent(element, 'open'), - bsHandler: elements => invokeBootstrapApi(elements, 'show'), + glHandler: (element) => tooltipsApp().triggerEvent(element, 'open'), + bsHandler: (elements) => invokeBootstrapApi(elements, 'show'), }); export const destroy = () => { tooltipsApp().$destroy(); diff --git a/app/assets/javascripts/tracking.js b/app/assets/javascripts/tracking.js index 0a1211d0a76..5d82d56f4ba 100644 --- a/app/assets/javascripts/tracking.js +++ b/app/assets/javascripts/tracking.js @@ -43,7 +43,7 @@ const eventHandler = (e, func, opts = {}) => { }; const eventHandlers = (category, func) => { - const handler = opts => e => eventHandler(e, func, { ...{ category }, ...opts }); + const handler = (opts) => (e) => eventHandler(e, func, { ...{ category }, ...opts }); const handlers = []; handlers.push({ name: 'click', func: handler() }); handlers.push({ name: 'show.bs.dropdown', func: handler({ suffix: '_show' }) }); @@ -79,7 +79,7 @@ export default class Tracking { parent.trackingBound = true; const handlers = eventHandlers(category, (...args) => this.event(...args)); - handlers.forEach(event => parent.addEventListener(event.name, event.func)); + handlers.forEach((event) => parent.addEventListener(event.name, event.func)); return handlers; } @@ -88,7 +88,7 @@ export default class Tracking { const loadEvents = parent.querySelectorAll('[data-track-event="render"]'); - loadEvents.forEach(element => { + loadEvents.forEach((element) => { const { action, data } = createEventPayload(element); this.event(category, action, data); }); diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js index 947246b2fbb..58bff370fa5 100644 --- a/app/assets/javascripts/tree.js +++ b/app/assets/javascripts/tree.js @@ -8,7 +8,7 @@ export default class TreeView { this.initKeyNav(); // Code browser tree slider // Make the entire tree-item row clickable, but not if clicking another link (like a commit message) - $('.tree-content-holder .tree-item').on('click', function(e) { + $('.tree-content-holder .tree-item').on('click', function (e) { const $clickedEl = $(e.target); const path = $('.tree-item-file-name a', this).attr('href'); if (!$clickedEl.is('a') && !$clickedEl.is('.str-truncated')) { @@ -20,15 +20,13 @@ export default class TreeView { } }); // Show the "Loading commit data" for only the first element - $('span.log_loading') - .first() - .removeClass('hide'); + $('span.log_loading').first().removeClass('hide'); } initKeyNav() { const li = $('tr.tree-item'); let liSelected = null; - return $('body').keydown(e => { + return $('body').keydown((e) => { let next, path; if ($('input:focus').length > 0 && (e.which === 38 || e.which === 40)) { return false; diff --git a/app/assets/javascripts/ui_development_kit.js b/app/assets/javascripts/ui_development_kit.js index 4f32e143de8..028b047d9f5 100644 --- a/app/assets/javascripts/ui_development_kit.js +++ b/app/assets/javascripts/ui_development_kit.js @@ -10,19 +10,19 @@ export default () => { { order_by: 'last_activity_at', }, - data => { + (data) => { callback(data); }, ); }, - text: project => project.name_with_namespace || project.name, + text: (project) => project.name_with_namespace || project.name, selectable: true, fieldName: 'author_id', filterable: true, search: { fields: ['name_with_namespace'], }, - id: data => data.id, - isSelected: data => data.id === 2, + id: (data) => data.id, + isSelected: (data) => data.id === 2, }); }; diff --git a/app/assets/javascripts/usage_ping_consent.js b/app/assets/javascripts/usage_ping_consent.js index a69620c1762..94d476d13ae 100644 --- a/app/assets/javascripts/usage_ping_consent.js +++ b/app/assets/javascripts/usage_ping_consent.js @@ -5,7 +5,7 @@ import { parseBoolean } from './lib/utils/common_utils'; import { __ } from './locale'; export default () => { - $('body').on('click', '.js-usage-consent-action', e => { + $('body').on('click', '.js-usage-consent-action', (e) => { e.preventDefault(); e.stopImmediatePropagation(); // overwrite rails listener diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js index df00f38dd70..44e54c85f3c 100644 --- a/app/assets/javascripts/user_callout.js +++ b/app/assets/javascripts/user_callout.js @@ -15,7 +15,7 @@ export default class UserCallout { init() { if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') { - this.userCalloutBody.find('.js-close-callout').on('click', e => this.dismissCallout(e)); + this.userCalloutBody.find('.js-close-callout').on('click', (e) => this.dismissCallout(e)); } } diff --git a/app/assets/javascripts/user_lists/components/user_list_form.vue b/app/assets/javascripts/user_lists/components/user_list_form.vue index 657acb51fee..a0364089d68 100644 --- a/app/assets/javascripts/user_lists/components/user_list_form.vue +++ b/app/assets/javascripts/user_lists/components/user_list_form.vue @@ -41,7 +41,7 @@ export default { ], }, translations: { - formLabel: s__('UserLists|Feature flag list'), + formLabel: s__('UserLists|Feature flag user list'), formSubtitle: s__( 'UserLists|Lists allow you to define a set of users to be used with feature flags. %{linkStart}Read more about feature flag lists.%{linkEnd}', ), diff --git a/app/assets/javascripts/user_lists/store/edit/actions.js b/app/assets/javascripts/user_lists/store/edit/actions.js index 8f0a2bafec7..6db2e65cf04 100644 --- a/app/assets/javascripts/user_lists/store/edit/actions.js +++ b/app/assets/javascripts/user_lists/store/edit/actions.js @@ -7,7 +7,7 @@ export const fetchUserList = ({ commit, state }) => { commit(types.REQUEST_USER_LIST); return Api.fetchFeatureFlagUserList(state.projectId, state.userListIid) .then(({ data }) => commit(types.RECEIVE_USER_LIST_SUCCESS, data)) - .catch(response => commit(types.RECEIVE_USER_LIST_ERROR, getErrorMessages(response))); + .catch((response) => commit(types.RECEIVE_USER_LIST_ERROR, getErrorMessages(response))); }; export const dismissErrorAlert = ({ commit }) => commit(types.DISMISS_ERROR_ALERT); @@ -18,5 +18,5 @@ export const updateUserList = ({ commit, state }, userList) => { name: userList.name, }) .then(({ data }) => redirectTo(data.path)) - .catch(response => commit(types.RECEIVE_USER_LIST_ERROR, getErrorMessages(response))); + .catch((response) => commit(types.RECEIVE_USER_LIST_ERROR, getErrorMessages(response))); }; diff --git a/app/assets/javascripts/user_lists/store/edit/index.js b/app/assets/javascripts/user_lists/store/edit/index.js index b30b0b04b9e..3b19b2b12ec 100644 --- a/app/assets/javascripts/user_lists/store/edit/index.js +++ b/app/assets/javascripts/user_lists/store/edit/index.js @@ -3,7 +3,7 @@ import createState from './state'; import * as actions from './actions'; import mutations from './mutations'; -export default initialState => +export default (initialState) => new Vuex.Store({ actions, mutations, diff --git a/app/assets/javascripts/user_lists/store/new/actions.js b/app/assets/javascripts/user_lists/store/new/actions.js index 185508bcfbc..478fca40142 100644 --- a/app/assets/javascripts/user_lists/store/new/actions.js +++ b/app/assets/javascripts/user_lists/store/new/actions.js @@ -11,5 +11,5 @@ export const createUserList = ({ commit, state }, userList) => { ...userList, }) .then(({ data }) => redirectTo(data.path)) - .catch(response => commit(types.RECEIVE_CREATE_USER_LIST_ERROR, getErrorMessages(response))); + .catch((response) => commit(types.RECEIVE_CREATE_USER_LIST_ERROR, getErrorMessages(response))); }; diff --git a/app/assets/javascripts/user_lists/store/new/index.js b/app/assets/javascripts/user_lists/store/new/index.js index b30b0b04b9e..3b19b2b12ec 100644 --- a/app/assets/javascripts/user_lists/store/new/index.js +++ b/app/assets/javascripts/user_lists/store/new/index.js @@ -3,7 +3,7 @@ import createState from './state'; import * as actions from './actions'; import mutations from './mutations'; -export default initialState => +export default (initialState) => new Vuex.Store({ actions, mutations, diff --git a/app/assets/javascripts/user_lists/store/show/actions.js b/app/assets/javascripts/user_lists/store/show/actions.js index 15b971aa5e8..dcd57efc146 100644 --- a/app/assets/javascripts/user_lists/store/show/actions.js +++ b/app/assets/javascripts/user_lists/store/show/actions.js @@ -5,7 +5,7 @@ import * as types from './mutation_types'; export const fetchUserList = ({ commit, state }) => { commit(types.REQUEST_USER_LIST); return Api.fetchFeatureFlagUserList(state.projectId, state.userListIid) - .then(response => commit(types.RECEIVE_USER_LIST_SUCCESS, response.data)) + .then((response) => commit(types.RECEIVE_USER_LIST_SUCCESS, response.data)) .catch(() => commit(types.RECEIVE_USER_LIST_ERROR)); }; @@ -27,6 +27,6 @@ export const updateUserList = ({ commit, state }) => { ...state.userList, user_xids: stringifyUserIds(state.userIds), }) - .then(response => commit(types.RECEIVE_USER_LIST_SUCCESS, response.data)) + .then((response) => commit(types.RECEIVE_USER_LIST_SUCCESS, response.data)) .catch(() => commit(types.RECEIVE_USER_LIST_ERROR)); }; diff --git a/app/assets/javascripts/user_lists/store/show/index.js b/app/assets/javascripts/user_lists/store/show/index.js index b30b0b04b9e..3b19b2b12ec 100644 --- a/app/assets/javascripts/user_lists/store/show/index.js +++ b/app/assets/javascripts/user_lists/store/show/index.js @@ -3,7 +3,7 @@ import createState from './state'; import * as actions from './actions'; import mutations from './mutations'; -export default initialState => +export default (initialState) => new Vuex.Store({ actions, mutations, diff --git a/app/assets/javascripts/user_lists/store/show/mutations.js b/app/assets/javascripts/user_lists/store/show/mutations.js index c3e766465a7..3cf3b2d8371 100644 --- a/app/assets/javascripts/user_lists/store/show/mutations.js +++ b/app/assets/javascripts/user_lists/store/show/mutations.js @@ -20,10 +20,10 @@ export default { [types.ADD_USER_IDS](state, ids) { state.userIds = [ ...state.userIds, - ...parseUserIds(ids).filter(id => id && !state.userIds.includes(id)), + ...parseUserIds(ids).filter((id) => id && !state.userIds.includes(id)), ]; }, [types.REMOVE_USER_ID](state, id) { - state.userIds = state.userIds.filter(uid => uid !== id); + state.userIds = state.userIds.filter((uid) => uid !== id); }, }; diff --git a/app/assets/javascripts/user_lists/store/utils.js b/app/assets/javascripts/user_lists/store/utils.js index f4e46947759..16f510bc32d 100644 --- a/app/assets/javascripts/user_lists/store/utils.js +++ b/app/assets/javascripts/user_lists/store/utils.js @@ -1,5 +1,6 @@ -export const parseUserIds = userIds => userIds.split(/\s*,\s*/g); +export const parseUserIds = (userIds) => userIds.split(/\s*,\s*/g); -export const stringifyUserIds = userIds => userIds.join(','); +export const stringifyUserIds = (userIds) => userIds.join(','); -export const getErrorMessages = error => [].concat(error?.response?.data?.message ?? error.message); +export const getErrorMessages = (error) => + [].concat(error?.response?.data?.message ?? error.message); diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js index 3521c1a105f..c18f4fb46cc 100644 --- a/app/assets/javascripts/user_popovers.js +++ b/app/assets/javascripts/user_popovers.js @@ -5,14 +5,14 @@ import { sanitize } from '~/lib/dompurify'; import UsersCache from './lib/utils/users_cache'; import UserPopover from './vue_shared/components/user_popover/user_popover.vue'; -const removeTitle = el => { +const removeTitle = (el) => { // Removing titles so its not showing tooltips also el.dataset.originalTitle = ''; el.setAttribute('title', ''); }; -const getPreloadedUserInfo = dataset => { +const getPreloadedUserInfo = (dataset) => { const userId = dataset.user || dataset.userId; const { username, name, avatarUrl } = dataset; @@ -28,7 +28,7 @@ const getPreloadedUserInfo = dataset => { * Adds a UserPopover component to the body, hands over as much data as the target element has in data attributes. * loads based on data-user-id more data about a user from the API and sets it on the popover */ -const populateUserInfo = user => { +const populateUserInfo = (user) => { const { userId } = user; return Promise.all([UsersCache.retrieveById(userId), UsersCache.retrieveStatusById(userId)]).then( @@ -66,7 +66,7 @@ export default (elements = document.querySelectorAll('.js-user-link')) => { return userLinks .filter(({ dataset }) => dataset.user || dataset.userId) - .map(el => { + .map((el) => { if (initializedPopovers.has(el)) { return initializedPopovers.get(el); } diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js index e693c3e90a4..79dc20fd498 100644 --- a/app/assets/javascripts/users_select/index.js +++ b/app/assets/javascripts/users_select/index.js @@ -75,7 +75,7 @@ function UsersSelect(currentUser, els, options = {}) { selectedId = selectedIdDefault; } - const assignYourself = function() { + const assignYourself = function () { const unassignedSelected = $dropdown .closest('.selectbox') .find(`input[name='${$dropdown.data('fieldName')}'][value=0]`); @@ -108,17 +108,17 @@ function UsersSelect(currentUser, els, options = {}) { $block[0].addEventListener('assignYourself', assignYourself); } - const getSelectedUserInputs = function() { + const getSelectedUserInputs = function () { return $selectbox.find(`input[name="${$dropdown.data('fieldName')}"]`); }; - const getSelected = function() { + const getSelected = function () { return getSelectedUserInputs() .map((index, input) => parseInt(input.value, 10)) .get(); }; - const checkMaxSelect = function() { + const checkMaxSelect = function () { const maxSelect = $dropdown.data('maxSelect'); if (maxSelect) { const selected = getSelected(); @@ -144,15 +144,15 @@ function UsersSelect(currentUser, els, options = {}) { } }; - const getMultiSelectDropdownTitle = function(selectedUser, isSelected) { - const selectedUsers = getSelected().filter(u => u !== 0); + const getMultiSelectDropdownTitle = function (selectedUser, isSelected) { + const selectedUsers = getSelected().filter((u) => u !== 0); const firstUser = getSelectedUserInputs() .map((index, input) => ({ name: input.dataset.meta, value: parseInt(input.value, 10), })) - .filter(u => u.id !== 0) + .filter((u) => u.id !== 0) .get(0); if (selectedUsers.length === 0) { @@ -160,7 +160,7 @@ function UsersSelect(currentUser, els, options = {}) { } else if (selectedUsers.length === 1) { return firstUser.name; } else if (isSelected) { - const otherSelected = selectedUsers.filter(s => s !== selectedUser.id); + const otherSelected = selectedUsers.filter((s) => s !== selectedUser.id); return sprintf(s__('UsersSelect|%{name} + %{length} more'), { name: selectedUser.name, length: otherSelected.length, @@ -172,7 +172,7 @@ function UsersSelect(currentUser, els, options = {}) { }); }; - $assignToMeLink.on('click', e => { + $assignToMeLink.on('click', (e) => { e.preventDefault(); $(e.currentTarget).hide(); @@ -196,12 +196,12 @@ function UsersSelect(currentUser, els, options = {}) { } }); - $block.on('click', '.js-assign-yourself', e => { + $block.on('click', '.js-assign-yourself', (e) => { e.preventDefault(); return assignTo(userSelect.currentUser.id); }); - assignTo = function(selected) { + assignTo = function (selected) { const data = {}; data[abilityName] = {}; data[abilityName].assignee_id = selected != null ? selected : null; @@ -250,7 +250,7 @@ function UsersSelect(currentUser, els, options = {}) { return initDeprecatedJQueryDropdown($dropdown, { showMenuAbove, data(term, callback) { - return userSelect.users(term, options, users => { + return userSelect.users(term, options, (users) => { // GitLabDropdownFilter returns this.instance // GitLabDropdownRemote returns this.options.instance const deprecatedJQueryDropdown = this.instance || this.options.instance; @@ -266,14 +266,14 @@ function UsersSelect(currentUser, els, options = {}) { // Potential duplicate entries when dealing with issue board // because issue board is also managed by vue - const selectedUsers = uniqBy(selectedInputs, a => a.value) - .filter(input => { + const selectedUsers = uniqBy(selectedInputs, (a) => a.value) + .filter((input) => { const userId = parseInt(input.value, 10); - const inUsersArray = users.find(u => u.id === userId); + const inUsersArray = users.find((u) => u.id === userId); return !inUsersArray && userId !== 0; }) - .map(input => { + .map((input) => { const userId = parseInt(input.value, 10); const { avatarUrl, avatar_url, name, username, canMerge } = input.dataset; return { @@ -334,7 +334,7 @@ function UsersSelect(currentUser, els, options = {}) { } if ($dropdown.hasClass('js-multiselect')) { - const selected = getSelected().filter(i => i !== 0); + const selected = getSelected().filter((i) => i !== 0); if (selected.length > 0) { if ($dropdown.data('dropdownHeader')) { @@ -346,12 +346,12 @@ function UsersSelect(currentUser, els, options = {}) { } const selectedUsers = users - .filter(u => selected.indexOf(u.id) !== -1) + .filter((u) => selected.indexOf(u.id) !== -1) .sort((a, b) => a.name > b.name); - users = users.filter(u => selected.indexOf(u.id) === -1); + users = users.filter((u) => selected.indexOf(u.id) === -1); - selectedUsers.forEach(selectedUser => { + selectedUsers.forEach((selectedUser) => { showDivider += 1; users.splice(showDivider, 0, selectedUser); }); @@ -477,7 +477,7 @@ function UsersSelect(currentUser, els, options = {}) { } } - if (getSelected().find(u => u === gon.current_user_id)) { + if (getSelected().find((u) => u === gon.current_user_id)) { $assignToMeLink.hide(); } else { $assignToMeLink.show(); @@ -544,7 +544,7 @@ function UsersSelect(currentUser, els, options = {}) { } if (selected.length > 0) { - getSelected().forEach(selectedId => highlightSelected(selectedId)); + getSelected().forEach((selectedId) => highlightSelected(selectedId)); } else if ($dropdown.hasClass('js-issue-board-sidebar')) { highlightSelected(0); } else { @@ -559,7 +559,7 @@ function UsersSelect(currentUser, els, options = {}) { let selected = false; if (this.multiSelect) { - selected = getSelected().find(u => user.id === u); + selected = getSelected().find((u) => user.id === u); const { fieldName } = this; const field = $dropdown @@ -613,7 +613,7 @@ function UsersSelect(currentUser, els, options = {}) { multiple: $(select).hasClass('multiselect'), minimumInputLength: 0, query(query) { - return userSelect.users(query.term, options, users => { + return userSelect.users(query.term, options, (users) => { let name; const data = { results: users, @@ -694,7 +694,7 @@ function UsersSelect(currentUser, els, options = {}) { } } -UsersSelect.prototype.initSelection = function(element, callback) { +UsersSelect.prototype.initSelection = function (element, callback) { const id = $(element).val(); if (id === '0') { const nullUser = { @@ -706,7 +706,7 @@ UsersSelect.prototype.initSelection = function(element, callback) { } }; -UsersSelect.prototype.formatResult = function(user) { +UsersSelect.prototype.formatResult = function (user) { let avatar = gon.default_avatar_url; if (user.avatar_url) { avatar = user.avatar_url; @@ -728,11 +728,11 @@ UsersSelect.prototype.formatResult = function(user) { `; }; -UsersSelect.prototype.formatSelection = function(user) { +UsersSelect.prototype.formatSelection = function (user) { return escape(user.name); }; -UsersSelect.prototype.user = function(user_id, callback) { +UsersSelect.prototype.user = function (user_id, callback) { if (!/^\d+$/.test(user_id)) { return false; } @@ -746,7 +746,7 @@ UsersSelect.prototype.user = function(user_id, callback) { // Return users list. Filtered by query // Only active users retrieved -UsersSelect.prototype.users = function(query, options, callback) { +UsersSelect.prototype.users = function (query, options, callback) { const url = this.buildUrl(this.usersPath); const params = { search: query, @@ -755,8 +755,8 @@ UsersSelect.prototype.users = function(query, options, callback) { }; const isMergeRequest = options.issuableType === 'merge_request'; - const isEditMergeRequest = !options.issuableType && (options.iid && options.targetBranch); - const isNewMergeRequest = !options.issuableType && (!options.iid && options.targetBranch); + const isEditMergeRequest = !options.issuableType && options.iid && options.targetBranch; + const isNewMergeRequest = !options.issuableType && !options.iid && options.targetBranch; if (isMergeRequest || isEditMergeRequest || isNewMergeRequest) { params.merge_request_iid = options.iid || null; @@ -772,14 +772,14 @@ UsersSelect.prototype.users = function(query, options, callback) { }); }; -UsersSelect.prototype.buildUrl = function(url) { +UsersSelect.prototype.buildUrl = function (url) { if (gon.relative_url_root != null) { url = gon.relative_url_root.replace(/\/$/, '') + url; } return url; }; -UsersSelect.prototype.renderRow = function( +UsersSelect.prototype.renderRow = function ( issuableType, user, selected, @@ -815,7 +815,7 @@ UsersSelect.prototype.renderRow = function( `; }; -UsersSelect.prototype.renderRowAvatar = function(issuableType, user, img) { +UsersSelect.prototype.renderRowAvatar = function (issuableType, user, img) { if (user.beforeDivider) { return img; } @@ -831,22 +831,22 @@ UsersSelect.prototype.renderRowAvatar = function(issuableType, user, img) { </span>`; }; -UsersSelect.prototype.renderApprovalRules = function(elsClassName, approvalRules = []) { - if (!gon.features?.reviewerApprovalRules || !elsClassName?.includes('reviewer')) { +UsersSelect.prototype.renderApprovalRules = function (elsClassName, approvalRules = []) { + const count = approvalRules.length; + + if (!gon.features?.reviewerApprovalRules || !elsClassName?.includes('reviewer') || !count) { return ''; } - const count = approvalRules.length; const [rule] = approvalRules; const countText = sprintf(__('(+%{count} rules)'), { count }); const renderApprovalRulesCount = count > 1 ? `<span class="ml-1">${countText}</span>` : ''; + const ruleName = rule.rule_type === 'code_owner' ? __('Code Owner') : rule.name; - return count - ? `<div class="gl-display-flex gl-font-sm"> - <span class="gl-text-truncate" title="${rule.name}">${rule.name}</span> - ${renderApprovalRulesCount} - </div>` - : ''; + return `<div class="gl-display-flex gl-font-sm"> + <span class="gl-text-truncate" title="${ruleName}">${ruleName}</span> + ${renderApprovalRulesCount} + </div>`; }; export default UsersSelect; diff --git a/app/assets/javascripts/vue_alerts.js b/app/assets/javascripts/vue_alerts.js index 6550eb31491..abc1dd75645 100644 --- a/app/assets/javascripts/vue_alerts.js +++ b/app/assets/javascripts/vue_alerts.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import { parseBoolean } from '~/lib/utils/common_utils'; import DismissibleAlert from '~/vue_shared/components/dismissible_alert.vue'; -const mountVueAlert = el => { +const mountVueAlert = (el) => { const props = { html: el.innerHTML, }; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue index 7297f8f8677..9b822657184 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue @@ -74,7 +74,7 @@ export default { return this.mr.approvals || {}; }, approvedBy() { - return this.approvals.approved_by ? this.approvals.approved_by.map(x => x.user) : []; + return this.approvals.approved_by ? this.approvals.approved_by.map((x) => x.user) : []; }, userHasApproved() { return Boolean(this.approvals.user_has_approved); @@ -136,7 +136,7 @@ export default { approveWithAuth(data) { this.updateApproval( () => this.service.approveMergeRequestWithAuth(data), - error => { + (error) => { if (error && error.response && error.response.status === 401) { this.hasApprovalAuthError = true; return; @@ -155,7 +155,7 @@ export default { this.isApproving = true; this.clearError(); return serviceFn() - .then(data => { + .then((data) => { this.mr.setApprovals(data); eventHub.$emit('MRWidgetUpdateRequested'); this.$emit('updated'); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue index b90cbfd1a1a..215df8acece 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue @@ -121,7 +121,7 @@ export default { this.actionInProgress = actionName; MRWidgetService.executeInlineAction(endpoint) - .then(resp => { + .then((resp) => { const redirectUrl = resp?.data?.redirect_url; if (redirectUrl) { visitUrl(redirectUrl); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue index e3c0b7935d7..abc831c8abe 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue @@ -57,7 +57,7 @@ export default { return this.deployment.changes && this.deployment.changes.length > 1; }, filteredChanges() { - return this.deployment?.changes?.filter(change => change.path.includes(this.searchTerm)); + return this.deployment?.changes?.filter((change) => change.path.includes(this.searchTerm)); }, }, }; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue index 9b2cd41092e..2f27216f2e9 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue @@ -102,7 +102,7 @@ export default { loadMetrics() { backOff((next, stop) => { MRWidgetService.fetchMetrics(this.metricsUrl) - .then(res => { + .then((res) => { if (res.status === statusCodes.NO_CONTENT) { this.backOffRequestCounter += 1; /* eslint-disable no-unused-expressions */ @@ -113,14 +113,14 @@ export default { }) .catch(stop); }) - .then(res => { + .then((res) => { if (res.status === statusCodes.NO_CONTENT) { return res; } return res.data; }) - .then(data => { + .then((data) => { this.computeGraphData(data.metrics, data.deployment_time); return data; }) @@ -145,7 +145,7 @@ export default { <template #metricsLink="{ content }"> <gl-link :href="metricsMonitoringUrl">{{ content }}</gl-link> </template> - <template #emphasis="{content}"> + <template #emphasis="{ content }"> <strong>{{ content }}</strong> </template> <template #memoryFrom>{{ memoryFrom }}</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue index eff26729fa7..33809b953ee 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue @@ -67,11 +67,11 @@ export default { }, mounted() { this.fetchCollapsedData(this.$props) - .then(data => { + .then((data) => { this.collapsedData = data; this.loadingState = null; }) - .catch(e => { + .catch((e) => { this.loadingState = LOADING_STATES.collapsedError; throw e; }); @@ -86,11 +86,11 @@ export default { this.loadingState = LOADING_STATES.expandedLoading; this.fetchFullData(this.$props) - .then(data => { + .then((data) => { this.loadingState = null; this.fullData = data; }) - .catch(e => { + .catch((e) => { this.loadingState = null; throw e; }); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js index 5014c12dc30..529160de6a7 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js @@ -11,7 +11,7 @@ export default { return h( 'div', {}, - extensions.map(extension => + extensions.map((extension) => h(extension, { props: extensions[0].props.reduce( (acc, key) => ({ diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js index 2bfaec8a1c9..9796bb44939 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js @@ -3,7 +3,7 @@ import ExtensionBase from './base.vue'; // Holds all the currently registered extensions export const extensions = []; -export const registerExtension = extension => { +export const registerExtension = (extension) => { // Pushes into the extenions array a dynamically created Vue component // that gets exteneded from `base.vue` extensions.push({ diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue index a2636ce52ad..560a68031ef 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue @@ -13,7 +13,7 @@ export default { type: String, required: false, default: DANGER, - validator: value => [WARNING, DANGER].includes(value), + validator: (value) => [WARNING, DANGER].includes(value), }, helpPath: { type: String, @@ -36,7 +36,7 @@ export default { </script> <template> - <div class="m-3 ml-7" :class="messageClass"> + <div class="gl-m-3 gl-ml-7" :class="messageClass"> <slot></slot> <gl-link v-if="helpPath" :href="helpPath" target="_blank"> <gl-icon :size="16" name="question-o" class="align-middle" /> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue index 1727383ea2c..3cd003461b3 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue @@ -30,7 +30,7 @@ export default { }; </script> <template> - <section class="mr-widget-help font-italic"> + <section class="gl-py-3 gl-pr-3 gl-pl-5 gl-ml-7 mr-widget-help gl-font-style-italic"> <template v-if="missingBranch"> {{ missingBranchInfo }} </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index 5066a88b52b..4c130945487 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -11,6 +11,7 @@ import { import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline'; import { s__, n__ } from '~/locale'; import PipelineStage from '~/pipelines/components/pipelines_list/stage.vue'; +import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; @@ -23,6 +24,7 @@ export default { GlIcon, GlSprintf, GlTooltip, + PipelineArtifacts, PipelineStage, TooltipOnTruncate, LinkedPipelinesMiniList: () => @@ -97,6 +99,9 @@ export default { hasCommitInfo() { return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0; }, + hasArtifacts() { + return this.pipeline?.details?.artifacts?.length > 0; + }, isMergeRequestPipeline() { return Boolean(this.pipeline.flags && this.pipeline.flags.merge_request_pipeline); }, @@ -142,7 +147,7 @@ export default { data-testid="ci-error-message" > <gl-sprintf :message="$options.errorText"> - <template #link="{content}"> + <template #link="{ content }"> <gl-link :href="mrTroubleshootingDocsPath">{{ content }}</gl-link> </template> </gl-sprintf> @@ -218,7 +223,6 @@ export default { data-testid="pipeline-coverage-delta" >({{ pipelineCoverageDelta }}%)</span > - {{ pipelineCoverageJobNumberText }} <span ref="pipelineCoverageQuestion"> <gl-icon name="question" :size="12" /> @@ -258,6 +262,11 @@ export default { </template> </span> <linked-pipelines-mini-list v-if="triggered.length" :triggered="triggered" /> + <pipeline-artifacts + v-if="hasArtifacts" + :artifacts="pipeline.details.artifacts" + class="gl-ml-3" + /> </span> </div> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue index 56a50b55f9d..43317130b08 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue @@ -30,7 +30,7 @@ export default { }; </script> <template> - <section class="mr-info-list mr-links"> + <section class="mr-info-list gl-ml-7 gl-pb-5"> <p v-if="relatedLinks.closing">{{ closesText }} <span v-html="relatedLinks.closing"></span></p> <p v-if="relatedLinks.mentioned"> {{ s__('mrWidget|Mentions') }} <span v-html="relatedLinks.mentioned"></span> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue index a9d148505e1..7acdd695cc2 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue @@ -86,12 +86,12 @@ export default { to create one.`) " > - <template #prefixToLink="{content}"> + <template #prefixToLink="{ content }"> <strong> {{ content }} </strong> </template> - <template #addPipelineLink="{content}"> + <template #addPipelineLink="{ content }"> <gl-link :href="pipelinePath" class="gl-ml-1" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue index c917b69953f..d50d97e3570 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue @@ -18,7 +18,7 @@ export default { </script> <template> - <p v-once class="mr-info-list mr-links gl-mb-0"> + <p v-once class="mr-info-list gl-ml-7 gl-pb-5 gl-mb-0"> <span class="status-text"> <gl-sprintf :message="$options.i18n.removesBranchText"> <template #strong="{ content }"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue index b6722de5277..73d75352cb5 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue @@ -25,12 +25,13 @@ export default { class="mr-commit-dropdown" > <gl-dropdown-item - v-for="commit in commits" - :key="commit.short_id" + v-for="(commit, index) in commits" + :key="index" class="text-nowrap text-truncate" @click="$emit('input', commit.message)" > - <span class="monospace mr-2">{{ commit.short_id }}</span> {{ commit.title }} + <span class="monospace mr-2">{{ commit.shortId || commit.short_id }}</span> + {{ commit.title }} </gl-dropdown-item> </gl-dropdown> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue index bdcea9871ea..d331f1690f5 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue @@ -83,6 +83,7 @@ export default { :aria-label="ariaLabel" category="tertiary" class="commit-edit-toggle gl-mr-3" + size="small" :icon="collapseIcon" @click.stop="toggle()" /> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue index 750014c599a..20ac8f5a467 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue @@ -1,21 +1,37 @@ <script> -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui'; import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge'; +import autoMergeEnabledQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { deprecatedCreateFlash as Flash } from '../../../flash'; import statusIcon from '../mr_widget_status_icon.vue'; import MrWidgetAuthor from '../mr_widget_author.vue'; import eventHub from '../../event_hub'; import { AUTO_MERGE_STRATEGIES } from '../../constants'; import { __ } from '~/locale'; +import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; export default { name: 'MRWidgetAutoMergeEnabled', + apollo: { + state: { + query: autoMergeEnabledQuery, + skip() { + return !this.glFeatures.mergeRequestWidgetGraphql; + }, + variables() { + return this.mergeRequestQueryVariables; + }, + update: (data) => data.project?.mergeRequest, + }, + }, components: { MrWidgetAuthor, statusIcon, GlLoadingIcon, + GlSkeletonLoader, }, - mixins: [autoMergeMixin], + mixins: [autoMergeMixin, glFeatureFlagMixin(), mergeRequestQueryVariablesMixin], props: { mr: { type: Object, @@ -30,20 +46,47 @@ export default { }, data() { return { + state: {}, isCancellingAutoMerge: false, isRemovingSourceBranch: false, }; }, computed: { + loading() { + return this.glFeatures.mergeRequestWidgetGraphql && this.$apollo.queries.state.loading; + }, + mergeUser() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + return this.state.mergeUser; + } + + return this.mr.setToAutoMergeBy; + }, + targetBranch() { + return (this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr).targetBranch; + }, + shouldRemoveSourceBranch() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + return this.state.shouldRemoveSourceBranch || this.state.forceRemoveSourceBranch; + } + + return this.mr.shouldRemoveSourceBranch; + }, + autoMergeStrategy() { + return (this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr).autoMergeStrategy; + }, canRemoveSourceBranch() { - const { - shouldRemoveSourceBranch, - canRemoveSourceBranch, - mergeUserId, - currentUserId, - } = this.mr; + const { currentUserId } = this.mr; + const mergeUserId = this.glFeatures.mergeRequestWidgetGraphql + ? this.state.mergeUser?.id + : this.mr.mergeUserId; + const canRemoveSourceBranch = this.glFeatures.mergeRequestWidgetGraphql + ? this.state.userPermissions.removeSourceBranch + : this.mr.canRemoveSourceBranch; - return !shouldRemoveSourceBranch && canRemoveSourceBranch && mergeUserId === currentUserId; + return ( + !this.shouldRemoveSourceBranch && canRemoveSourceBranch && mergeUserId === currentUserId + ); }, }, methods: { @@ -51,8 +94,8 @@ export default { this.isCancellingAutoMerge = true; this.service .cancelAutomaticMerge() - .then(res => res.data) - .then(data => { + .then((res) => res.data) + .then((data) => { eventHub.$emit('UpdateWidgetData', data); }) .catch(() => { @@ -63,15 +106,15 @@ export default { removeSourceBranch() { const options = { sha: this.mr.sha, - auto_merge_strategy: this.mr.autoMergeStrategy, + auto_merge_strategy: this.autoMergeStrategy, should_remove_source_branch: true, }; this.isRemovingSourceBranch = true; this.service .merge(options) - .then(res => res.data) - .then(data => { + .then((res) => res.data) + .then((data) => { if (AUTO_MERGE_STRATEGIES.includes(data.status)) { eventHub.$emit('MRWidgetUpdateRequested'); } @@ -86,49 +129,64 @@ export default { </script> <template> <div class="mr-widget-body media"> - <status-icon status="success" /> - <div class="media-body"> - <h4 class="d-flex align-items-start"> - <span class="gl-mr-3"> - <span class="js-status-text-before-author">{{ statusTextBeforeAuthor }}</span> - <mr-widget-author :author="mr.setToAutoMergeBy" /> - <span class="js-status-text-after-author">{{ statusTextAfterAuthor }}</span> - </span> - <a - v-if="mr.canCancelAutomaticMerge" - :disabled="isCancellingAutoMerge" - role="button" - href="#" - class="btn btn-sm btn-default js-cancel-auto-merge" - @click.prevent="cancelAutomaticMerge" - > - <gl-loading-icon v-if="isCancellingAutoMerge" inline class="gl-mr-1" /> - {{ cancelButtonText }} - </a> - </h4> - <section class="mr-info-list"> - <p> - {{ s__('mrWidget|The changes will be merged into') }} - <a :href="mr.targetBranchPath" class="label-branch">{{ mr.targetBranch }}</a> - </p> - <p v-if="mr.shouldRemoveSourceBranch"> - {{ s__('mrWidget|The source branch will be deleted') }} - </p> - <p v-else class="d-flex align-items-start"> - <span class="gl-mr-3">{{ s__('mrWidget|The source branch will not be deleted') }}</span> + <div v-if="loading" class="gl-w-full mr-conflict-loader"> + <gl-skeleton-loader :width="334" :height="30"> + <rect x="0" y="3" width="24" height="24" rx="4" /> + <rect x="32" y="7" width="150" height="16" rx="4" /> + <rect x="190" y="7" width="144" height="16" rx="4" /> + </gl-skeleton-loader> + </div> + <template v-else> + <status-icon status="success" /> + <div class="media-body"> + <h4 class="gl-display-flex"> + <span class="gl-mr-3"> + <span class="js-status-text-before-author" data-testid="beforeStatusText">{{ + statusTextBeforeAuthor + }}</span> + <mr-widget-author :author="mergeUser" /> + <span class="js-status-text-after-author" data-testid="afterStatusText">{{ + statusTextAfterAuthor + }}</span> + </span> <a - v-if="canRemoveSourceBranch" - :disabled="isRemovingSourceBranch" + v-if="mr.canCancelAutomaticMerge" + :disabled="isCancellingAutoMerge" role="button" - class="btn btn-sm btn-default js-remove-source-branch" href="#" - @click.prevent="removeSourceBranch" + class="btn btn-sm btn-default js-cancel-auto-merge" + data-testid="cancelAutomaticMergeButton" + @click.prevent="cancelAutomaticMerge" > - <gl-loading-icon v-if="isRemovingSourceBranch" inline class="gl-mr-1" /> - {{ s__('mrWidget|Delete source branch') }} + <gl-loading-icon v-if="isCancellingAutoMerge" inline class="gl-mr-1" /> + {{ cancelButtonText }} </a> - </p> - </section> - </div> + </h4> + <section class="mr-info-list"> + <p> + {{ s__('mrWidget|The changes will be merged into') }} + <a :href="mr.targetBranchPath" class="label-branch">{{ targetBranch }}</a> + </p> + <p v-if="shouldRemoveSourceBranch"> + {{ s__('mrWidget|The source branch will be deleted') }} + </p> + <p v-else class="gl-display-flex"> + <span class="gl-mr-3">{{ s__('mrWidget|The source branch will not be deleted') }}</span> + <a + v-if="canRemoveSourceBranch" + :disabled="isRemovingSourceBranch" + role="button" + class="btn btn-sm btn-default js-remove-source-branch" + href="#" + data-testid="removeSourceBranchButton" + @click.prevent="removeSourceBranch" + > + <gl-loading-icon v-if="isRemovingSourceBranch" inline class="gl-mr-1" /> + {{ s__('mrWidget|Delete source branch') }} + </a> + </p> + </section> + </div> + </template> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue index 30da9947859..a2771bc4bfb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue @@ -1,7 +1,10 @@ <script> import { GlLoadingIcon, GlButton } from '@gitlab/ui'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import eventHub from '../../event_hub'; import statusIcon from '../mr_widget_status_icon.vue'; +import autoMergeFailedQuery from '../../queries/states/auto_merge_failed.query.graphql'; +import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; export default { name: 'MRWidgetAutoMergeFailed', @@ -10,6 +13,19 @@ export default { GlLoadingIcon, GlButton, }, + mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin], + apollo: { + mergeError: { + query: autoMergeFailedQuery, + skip() { + return !this.glFeatures.mergeRequestWidgetGraphql; + }, + variables() { + return this.mergeRequestQueryVariables; + }, + update: (data) => data.project?.mergeRequest?.mergeError, + }, + }, props: { mr: { type: Object, @@ -18,6 +34,7 @@ export default { }, data() { return { + mergeError: this.glFeatures.mergeRequestWidgetGraphql ? null : this.mr.mergeError, isRefreshing: false, }; }, @@ -36,7 +53,7 @@ export default { <status-icon status="warning" /> <div class="media-body space-children gl-display-flex gl-flex-wrap gl-align-items-center"> <span class="bold"> - <template v-if="mr.mergeError">{{ mr.mergeError }}</template> + <template v-if="mergeError">{{ mergeError }}</template> {{ s__('mrWidget|This merge request failed to be merged automatically') }} </span> <gl-button diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue index 87c59e5ece9..3d5daa4979b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue @@ -30,7 +30,7 @@ export default { variables() { return this.mergeRequestQueryVariables; }, - update: data => data.project.mergeRequest.userPermissions, + update: (data) => data.project.mergeRequest.userPermissions, }, stateData: { query: conflictsStateQuery, @@ -40,7 +40,7 @@ export default { variables() { return this.mergeRequestQueryVariables; }, - update: data => data.project.mergeRequest, + update: (data) => data.project.mergeRequest, }, }, props: { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue index 17cd740ddd9..9d646dbfb3e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -83,8 +83,8 @@ export default { this.service .removeSourceBranch() - .then(res => res.data) - .then(data => { + .then((res) => res.data) + .then((data) => { // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26 // eslint-disable-next-line @gitlab/require-i18n-strings if (data.message === 'Branch was deleted') { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue index 8511797286d..3f68979bc0e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue @@ -25,7 +25,7 @@ export default { variables() { return this.mergeRequestQueryVariables; }, - update: data => data.project.mergeRequest, + update: (data) => data.project.mergeRequest, }, }, props: { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index 14c2e9fa828..bf86e0d8b07 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -1,19 +1,36 @@ <script> /* eslint-disable vue/no-v-html */ -import { GlButton } from '@gitlab/ui'; +import { GlButton, GlSkeletonLoader } from '@gitlab/ui'; import { escape } from 'lodash'; +import { __, sprintf } from '~/locale'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import simplePoll from '../../../lib/utils/simple_poll'; import eventHub from '../../event_hub'; import statusIcon from '../mr_widget_status_icon.vue'; +import rebaseQuery from '../../queries/states/ready_to_merge.query.graphql'; +import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; import { deprecatedCreateFlash as Flash } from '../../../flash'; -import { __, sprintf } from '~/locale'; export default { name: 'MRWidgetRebase', + apollo: { + state: { + query: rebaseQuery, + skip() { + return !this.glFeatures.mergeRequestWidgetGraphql; + }, + variables() { + return this.mergeRequestQueryVariables; + }, + update: (data) => data.project.mergeRequest, + }, + }, components: { statusIcon, GlButton, + GlSkeletonLoader, }, + mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin], props: { mr: { type: Object, @@ -26,16 +43,41 @@ export default { }, data() { return { + state: {}, isMakingRequest: false, rebasingError: null, }; }, computed: { + isLoading() { + return this.glFeatures.mergeRequestWidgetGraphql && this.$apollo.queries.state.loading; + }, + rebaseInProgress() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + return this.state.rebaseInProgress; + } + + return this.mr.rebaseInProgress; + }, + canPushToSourceBranch() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + return this.state.userPermissions.pushToSourceBranch; + } + + return this.mr.canPushToSourceBranch; + }, + targetBranch() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + return this.state.targetBranch; + } + + return this.mr.targetBranch; + }, status() { - if (this.mr.rebaseInProgress || this.isMakingRequest) { + if (this.rebaseInProgress || this.isMakingRequest) { return 'loading'; } - if (!this.mr.canPushToSourceBranch && !this.mr.rebaseInProgress) { + if (!this.canPushToSourceBranch && !this.rebaseInProgress) { return 'warning'; } return 'success'; @@ -49,7 +91,7 @@ export default { 'Fast-forward merge is not possible. Rebase the source branch onto %{targetBranch} to allow this merge request to be merged.', ), { - targetBranch: `<span class="label-branch">${escape(this.mr.targetBranch)}</span>`, + targetBranch: `<span class="label-branch">${escape(this.targetBranch)}</span>`, }, false, ); @@ -65,7 +107,7 @@ export default { .then(() => { simplePoll(this.checkRebaseStatus); }) - .catch(error => { + .catch((error) => { this.isMakingRequest = false; if (error.response && error.response.data && error.response.data.merge_error) { @@ -78,8 +120,8 @@ export default { checkRebaseStatus(continuePolling, stopPolling) { this.service .poll() - .then(res => res.data) - .then(res => { + .then((res) => res.data) + .then((res) => { if (res.rebase_in_progress) { continuePolling(); } else { @@ -105,17 +147,30 @@ export default { </script> <template> <div class="mr-widget-body media"> - <status-icon :status="status" :show-disabled-button="showDisabledButton" /> + <div v-if="isLoading" class="gl-w-full mr-conflict-loader"> + <gl-skeleton-loader :width="334" :height="30"> + <rect x="0" y="3" width="24" height="24" rx="4" /> + <rect x="32" y="5" width="302" height="20" rx="4" /> + </gl-skeleton-loader> + </div> + <template v-else> + <status-icon :status="status" :show-disabled-button="showDisabledButton" /> - <div class="rebase-state-find-class-convention media media-body space-children"> - <template v-if="mr.rebaseInProgress || isMakingRequest"> - <span class="bold" data-testid="rebase-message">{{ __('Rebase in progress') }}</span> - </template> - <template v-if="!mr.rebaseInProgress && !mr.canPushToSourceBranch"> - <span class="bold" data-testid="rebase-message" v-html="fastForwardMergeText"></span> - </template> - <template v-if="!mr.rebaseInProgress && mr.canPushToSourceBranch && !isMakingRequest"> + <div class="rebase-state-find-class-convention media media-body space-children"> + <span + v-if="rebaseInProgress || isMakingRequest" + class="gl-font-weight-bold gl-ml-0!" + data-testid="rebase-message" + >{{ __('Rebase in progress') }}</span + > + <span + v-if="!rebaseInProgress && !canPushToSourceBranch" + class="gl-font-weight-bold gl-ml-0!" + data-testid="rebase-message" + v-html="fastForwardMergeText" + ></span> <div + v-if="!rebaseInProgress && canPushToSourceBranch && !isMakingRequest" class="accept-merge-holder clearfix js-toggle-container accept-action media space-children" > <gl-button @@ -126,14 +181,21 @@ export default { > {{ __('Rebase') }} </gl-button> - <span v-if="!rebasingError" class="bold" data-testid="rebase-message">{{ - __( - 'Fast-forward merge is not possible. Rebase the source branch onto the target branch.', - ) + <span + v-if="!rebasingError" + class="gl-font-weight-bold gl-ml-0!" + data-testid="rebase-message" + >{{ + __( + 'Fast-forward merge is not possible. Rebase the source branch onto the target branch.', + ) + }}</span + > + <span v-else class="gl-font-weight-bold danger gl-ml-0!" data-testid="rebase-message">{{ + rebasingError }}</span> - <span v-else class="bold danger" data-testid="rebase-message">{{ rebasingError }}</span> </div> - </template> - </div> + </div> + </template> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index 2c1f2285dda..a890b176df0 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -9,13 +9,18 @@ import { GlSprintf, GlLink, GlTooltipDirective, + GlSkeletonLoader, } from '@gitlab/ui'; import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge'; +import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql'; import simplePoll from '~/lib/utils/simple_poll'; import { __ } from '~/locale'; import MergeRequest from '../../../merge_request'; import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; import { deprecatedCreateFlash as Flash } from '../../../flash'; +import MergeRequestStore from '../../stores/mr_widget_store'; import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; import SquashBeforeMerge from './squash_before_merge.vue'; @@ -35,6 +40,31 @@ const MERGE_HOOK_VALIDATION_ERROR_STATUS = 'hook_validation_error'; export default { name: 'ReadyToMerge', + apollo: { + state: { + query: readyToMergeQuery, + skip() { + return !this.glFeatures.mergeRequestWidgetGraphql; + }, + variables() { + return this.mergeRequestQueryVariables; + }, + manual: true, + result({ data }) { + this.state = { + ...data.project.mergeRequest, + mergeRequestsFfOnlyEnabled: data.mergeRequestsFfOnlyEnabled, + onlyAllowMergeIfPipelineSucceeds: data.onlyAllowMergeIfPipelineSucceeds, + }; + this.removeSourceBranch = data.project.mergeRequest.shouldRemoveSourceBranch; + this.commitMessage = data.project.mergeRequest.defaultMergeCommitMessage; + this.squashBeforeMerge = data.project.mergeRequest.squashOnMerge; + this.isSquashReadOnly = data.project.squashReadOnly; + this.squashCommitMessage = data.project.mergeRequest.defaultSquashCommitMessage; + this.loading = false; + }, + }, + }, components: { statusIcon, SquashBeforeMerge, @@ -48,6 +78,7 @@ export default { GlButtonGroup, GlDropdown, GlDropdownItem, + GlSkeletonLoader, MergeTrainHelperText: () => import('ee_component/vue_merge_request_widget/components/merge_train_helper_text.vue'), MergeImmediatelyConfirmationDialog: () => @@ -58,13 +89,15 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, - mixins: [readyToMergeMixin], + mixins: [readyToMergeMixin, glFeatureFlagMixin(), mergeRequestQueryVariablesMixin], props: { mr: { type: Object, required: true }, service: { type: Object, required: true }, }, data() { return { + loading: this.glFeatures.mergeRequestWidgetGraphql, + state: {}, removeSourceBranch: this.mr.shouldRemoveSourceBranch, isMakingRequest: false, isMergingImmediately: false, @@ -75,13 +108,93 @@ export default { }; }, computed: { + stateData() { + return this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr; + }, + hasCI() { + return this.stateData.hasCI || this.stateData.hasCi; + }, isAutoMergeAvailable() { - return !isEmpty(this.mr.availableAutoMergeStrategies); + return !isEmpty(this.stateData.availableAutoMergeStrategies); + }, + pipeline() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + return this.state.pipelines?.nodes?.[0]; + } + + return this.mr.pipeline; + }, + isPipelineFailed() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + return ['FAILED', 'CANCELED'].indexOf(this.pipeline?.status) !== -1; + } + + return this.mr.isPipelineFailed; + }, + isMergeAllowed() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + return this.state.mergeable || false; + } + + return this.mr.isMergeAllowed; + }, + canRemoveSourceBranch() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + return this.state.userPermissions.removeSourceBranch; + } + + return this.mr.canRemoveSourceBranch; + }, + commits() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + return this.state.commitsWithoutMergeCommits.nodes; + } + + return this.mr.commits; + }, + commitsCount() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + return this.state.commitCount || 0; + } + + return this.mr.commitsCount; + }, + preferredAutoMergeStrategy() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + return MergeRequestStore.getPreferredAutoMergeStrategy( + this.state.availableAutoMergeStrategies, + ); + } + + return this.mr.preferredAutoMergeStrategy; + }, + isSHAMismatch() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + return this.mr.sha !== this.state.diffHeadSha; + } + + return this.mr.isSHAMismatch; + }, + squashIsSelected() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + return this.squashReadOnly ? this.state.squashOnMerge : this.state.squash; + } + + return this.mr.squashIsSelected; + }, + isPipelineActive() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + return this.pipeline?.active || false; + } + + return this.mr.isPipelineActive; }, status() { - const { pipeline, isPipelineFailed, hasCI, ciStatus } = this.mr; + const ciStatus = this.glFeatures.mergeRequestWidgetGraphql + ? this.pipeline?.status.toLowerCase() + : this.mr.ciStatus; - if ((hasCI && !ciStatus) || this.hasPipelineMustSucceedConflict) { + if ((this.hasCI && !ciStatus) || this.hasPipelineMustSucceedConflict) { return PIPELINE_FAILED_STATE; } @@ -89,7 +202,7 @@ export default { return PIPELINE_PENDING_STATE; } - if (pipeline && isPipelineFailed) { + if (this.pipeline && this.isPipelineFailed) { return PIPELINE_FAILED_STATE; } @@ -114,7 +227,7 @@ export default { if ( this.status === PIPELINE_FAILED_STATE || !this.commitMessage.length || - !this.mr.isMergeAllowed || + !this.isMergeAllowed || this.mr.preventMerge ) { return WARNING; @@ -133,27 +246,31 @@ export default { return __('Merge'); }, hasPipelineMustSucceedConflict() { - return !this.mr.hasCI && this.mr.onlyAllowMergeIfPipelineSucceeds; + return !this.hasCI && this.stateData.onlyAllowMergeIfPipelineSucceeds; }, isRemoveSourceBranchButtonDisabled() { return this.isMergeButtonDisabled; }, shouldShowSquashBeforeMerge() { - const { commitsCount, enableSquashBeforeMerge, squashIsReadonly, squashIsSelected } = this.mr; + const { enableSquashBeforeMerge } = this.mr; - if (squashIsReadonly && !squashIsSelected) { + if (this.isSquashReadOnly && !this.squashIsSelected) { return false; } - return enableSquashBeforeMerge && commitsCount > 1; + return enableSquashBeforeMerge && this.commitsCount > 1; }, shouldShowMergeControls() { - return this.mr.isMergeAllowed || this.isAutoMergeAvailable; + return this.isMergeAllowed || this.isAutoMergeAvailable; }, shouldShowSquashEdit() { return this.squashBeforeMerge && this.shouldShowSquashBeforeMerge; }, shouldShowMergeEdit() { + if (this.glFeatures.mergeRequestWidgetGraphql) { + return !this.state.mergeRequestsFfOnlyEnabled; + } + return !this.mr.ffOnlyEnabled; }, shaMismatchLink() { @@ -162,18 +279,26 @@ export default { }, methods: { updateMergeCommitMessage(includeDescription) { - const { commitMessageWithDescription, commitMessage } = this.mr; + const commitMessage = this.glFeatures.mergeRequestWidgetGraphql + ? this.state.defaultMergeCommitMessage + : this.mr.commitMessage; + const commitMessageWithDescription = this.glFeatures.mergeRequestWidgetGraphql + ? this.state.defaultMergeCommitMessageWithDescription + : this.mr.commitMessageWithDescription; this.commitMessage = includeDescription ? commitMessageWithDescription : commitMessage; }, handleMergeButtonClick(useAutoMerge, mergeImmediately = false) { if (mergeImmediately) { this.isMergingImmediately = true; } + const latestSha = this.glFeatures.mergeRequestWidgetGraphql + ? this.state.diffHeadSha + : this.mr.latestSHA; const options = { - sha: this.mr.latestSHA || this.mr.sha, + sha: latestSha || this.mr.sha, commit_message: this.commitMessage, - auto_merge_strategy: useAutoMerge ? this.mr.preferredAutoMergeStrategy : undefined, + auto_merge_strategy: useAutoMerge ? this.preferredAutoMergeStrategy : undefined, should_remove_source_branch: this.removeSourceBranch === true, squash: this.squashBeforeMerge, }; @@ -188,8 +313,8 @@ export default { this.isMakingRequest = true; this.service .merge(options) - .then(res => res.data) - .then(data => { + .then((res) => res.data) + .then((data) => { const hasError = data.status === MERGE_FAILED_STATUS || data.status === MERGE_HOOK_VALIDATION_ERROR_STATUS; @@ -228,8 +353,8 @@ export default { handleMergePolling(continuePolling, stopPolling) { this.service .poll() - .then(res => res.data) - .then(data => { + .then((res) => res.data) + .then((data) => { if (data.state === 'merged') { // If state is merged we should update the widget and stop the polling eventHub.$emit('MRWidgetUpdateRequested'); @@ -270,8 +395,8 @@ export default { handleRemoveBranchPolling(continuePolling, stopPolling) { this.service .poll() - .then(res => res.data) - .then(data => { + .then((res) => res.data) + .then((data) => { // If source branch exists then we should continue polling // because removing a source branch is a background task and takes time if (data.source_branch_exists) { @@ -294,156 +419,168 @@ export default { <template> <div> - <div class="mr-widget-body media" :class="{ 'gl-pb-3': shouldRenderMergeTrainHelperText }"> - <status-icon :status="iconClass" /> - <div class="media-body"> - <div class="mr-widget-body-controls media space-children"> - <gl-button-group> - <gl-button - size="medium" - category="primary" - class="qa-merge-button accept-merge-request" - :variant="mergeButtonVariant" - :disabled="isMergeButtonDisabled" - :loading="isMakingRequest" - @click="handleMergeButtonClick(isAutoMergeAvailable)" - >{{ mergeButtonText }}</gl-button - > - <gl-dropdown - v-if="shouldShowMergeImmediatelyDropdown" - v-gl-tooltip.hover.focus="__('Select merge moment')" - :disabled="isMergeButtonDisabled" - variant="info" - data-qa-selector="merge_moment_dropdown" - toggle-class="btn-icon js-merge-moment" - > - <template #button-content> - <gl-icon name="chevron-down" class="mr-0" /> - <span class="sr-only">{{ __('Select merge moment') }}</span> - </template> - <gl-dropdown-item - icon-name="warning" - button-class="accept-merge-request js-merge-immediately-button" - data-qa-selector="merge_immediately_option" - @click="handleMergeImmediatelyButtonClick" + <div v-if="loading" class="mr-widget-body"> + <div class="gl-w-full mr-ready-to-merge-loader"> + <gl-skeleton-loader :width="418" :height="30"> + <rect x="0" y="3" width="24" height="24" rx="4" /> + <rect x="32" y="0" width="70" height="30" rx="4" /> + <rect x="110" y="7" width="150" height="16" rx="4" /> + <rect x="268" y="7" width="150" height="16" rx="4" /> + </gl-skeleton-loader> + </div> + </div> + <template v-else> + <div class="mr-widget-body media" :class="{ 'gl-pb-3': shouldRenderMergeTrainHelperText }"> + <status-icon :status="iconClass" /> + <div class="media-body"> + <div class="mr-widget-body-controls media space-children"> + <gl-button-group> + <gl-button + size="medium" + category="primary" + class="qa-merge-button accept-merge-request" + :variant="mergeButtonVariant" + :disabled="isMergeButtonDisabled" + :loading="isMakingRequest" + @click="handleMergeButtonClick(isAutoMergeAvailable)" + >{{ mergeButtonText }}</gl-button > - {{ __('Merge immediately') }} - </gl-dropdown-item> - <merge-immediately-confirmation-dialog - ref="confirmationDialog" - :docs-url="mr.mergeImmediatelyDocsPath" - @mergeImmediately="onMergeImmediatelyConfirmation" - /> - </gl-dropdown> - </gl-button-group> - <div class="media-body-wrap space-children"> - <template v-if="shouldShowMergeControls"> - <label v-if="mr.canRemoveSourceBranch"> - <input - id="remove-source-branch-input" - v-model="removeSourceBranch" - :disabled="isRemoveSourceBranchButtonDisabled" - class="js-remove-source-branch-checkbox" - type="checkbox" + <gl-dropdown + v-if="shouldShowMergeImmediatelyDropdown" + v-gl-tooltip.hover.focus="__('Select merge moment')" + :disabled="isMergeButtonDisabled" + variant="info" + data-qa-selector="merge_moment_dropdown" + toggle-class="btn-icon js-merge-moment" + > + <template #button-content> + <gl-icon name="chevron-down" class="mr-0" /> + <span class="sr-only">{{ __('Select merge moment') }}</span> + </template> + <gl-dropdown-item + icon-name="warning" + button-class="accept-merge-request js-merge-immediately-button" + data-qa-selector="merge_immediately_option" + @click="handleMergeImmediatelyButtonClick" + > + {{ __('Merge immediately') }} + </gl-dropdown-item> + <merge-immediately-confirmation-dialog + ref="confirmationDialog" + :docs-url="mr.mergeImmediatelyDocsPath" + @mergeImmediately="onMergeImmediatelyConfirmation" /> - {{ __('Delete source branch') }} - </label> + </gl-dropdown> + </gl-button-group> + <div class="media-body-wrap space-children"> + <template v-if="shouldShowMergeControls"> + <label v-if="canRemoveSourceBranch"> + <input + id="remove-source-branch-input" + v-model="removeSourceBranch" + :disabled="isRemoveSourceBranchButtonDisabled" + class="js-remove-source-branch-checkbox" + type="checkbox" + /> + {{ __('Delete source branch') }} + </label> - <!-- Placeholder for EE extension of this component --> - <squash-before-merge - v-if="shouldShowSquashBeforeMerge" - v-model="squashBeforeMerge" - :help-path="mr.squashBeforeMergeHelpPath" - :is-disabled="isSquashReadOnly" - /> - </template> - <template v-else> - <div class="bold js-resolve-mr-widget-items-message"> - <div - v-if="hasPipelineMustSucceedConflict" - class="gl-display-flex gl-align-items-center" - data-testid="pipeline-succeed-conflict" - > - <gl-sprintf :message="pipelineMustSucceedConflictText" /> - <gl-link - :href="mr.pipelineMustSucceedDocsPath" - target="_blank" - class="gl-display-flex gl-ml-2" + <!-- Placeholder for EE extension of this component --> + <squash-before-merge + v-if="shouldShowSquashBeforeMerge" + v-model="squashBeforeMerge" + :help-path="mr.squashBeforeMergeHelpPath" + :is-disabled="isSquashReadOnly" + /> + </template> + <template v-else> + <div class="bold js-resolve-mr-widget-items-message"> + <div + v-if="hasPipelineMustSucceedConflict" + class="gl-display-flex gl-align-items-center" + data-testid="pipeline-succeed-conflict" > - <gl-icon name="question" /> - </gl-link> + <gl-sprintf :message="pipelineMustSucceedConflictText" /> + <gl-link + :href="mr.pipelineMustSucceedDocsPath" + target="_blank" + class="gl-display-flex gl-ml-2" + > + <gl-icon name="question" /> + </gl-link> + </div> + <gl-sprintf v-else :message="mergeDisabledText" /> </div> - <gl-sprintf v-else :message="mergeDisabledText" /> - </div> - </template> - </div> - </div> - <div v-if="mr.isSHAMismatch" class="d-flex align-items-center mt-2 js-sha-mismatch"> - <gl-icon name="warning-solid" class="text-warning mr-1" /> - <span class="text-warning"> - <gl-sprintf - :message=" - __('New changes were added. %{linkStart}Reload the page to review them%{linkEnd}') - " - > - <template #link="{ content }"> - <gl-link :href="mr.mergeRequestDiffsPath">{{ content }}</gl-link> </template> - </gl-sprintf> - </span> + </div> + </div> + <div v-if="isSHAMismatch" class="d-flex align-items-center mt-2 js-sha-mismatch"> + <gl-icon name="warning-solid" class="text-warning mr-1" /> + <span class="text-warning"> + <gl-sprintf + :message=" + __('New changes were added. %{linkStart}Reload the page to review them%{linkEnd}') + " + > + <template #link="{ content }"> + <gl-link :href="mr.mergeRequestDiffsPath">{{ content }}</gl-link> + </template> + </gl-sprintf> + </span> + </div> </div> </div> - </div> - <merge-train-helper-text - v-if="shouldRenderMergeTrainHelperText" - :pipeline-id="mr.pipeline.id" - :pipeline-link="mr.pipeline.path" - :merge-train-length="mr.mergeTrainsCount" - :merge-train-when-pipeline-succeeds-docs-path="mr.mergeTrainWhenPipelineSucceedsDocsPath" - /> - <template v-if="shouldShowMergeControls"> - <div v-if="mr.ffOnlyEnabled" class="mr-fast-forward-message"> - {{ __('Fast-forward merge without a merge commit') }} - </div> - <commits-header - v-if="shouldShowSquashEdit || shouldShowMergeEdit" - :is-squash-enabled="squashBeforeMerge" - :commits-count="mr.commitsCount" - :target-branch="mr.targetBranch" - :is-fast-forward-enabled="mr.ffOnlyEnabled" - :class="{ 'border-bottom': mr.mergeError }" - > - <ul class="border-top content-list commits-list flex-list"> - <commit-edit - v-if="shouldShowSquashEdit" - v-model="squashCommitMessage" - :label="__('Squash commit message')" - input-id="squash-message-edit" - squash - > - <commit-message-dropdown - slot="header" + <merge-train-helper-text + v-if="shouldRenderMergeTrainHelperText" + :pipeline-id="pipeline.id" + :pipeline-link="pipeline.path" + :merge-train-length="stateData.mergeTrainsCount" + :merge-train-when-pipeline-succeeds-docs-path="mr.mergeTrainWhenPipelineSucceedsDocsPath" + /> + <template v-if="shouldShowMergeControls"> + <div v-if="!shouldShowMergeEdit" class="mr-fast-forward-message"> + {{ __('Fast-forward merge without a merge commit') }} + </div> + <commits-header + v-if="shouldShowSquashEdit || shouldShowMergeEdit" + :is-squash-enabled="squashBeforeMerge" + :commits-count="commitsCount" + :target-branch="stateData.targetBranch" + :is-fast-forward-enabled="!shouldShowMergeEdit" + :class="{ 'border-bottom': stateData.mergeError }" + > + <ul class="border-top content-list commits-list flex-list"> + <commit-edit + v-if="shouldShowSquashEdit" v-model="squashCommitMessage" - :commits="mr.commits" - /> - </commit-edit> - <commit-edit - v-if="shouldShowMergeEdit" - v-model="commitMessage" - :label="__('Merge commit message')" - input-id="merge-message-edit" - > - <label slot="checkbox"> - <input - id="include-description" - type="checkbox" - @change="updateMergeCommitMessage($event.target.checked)" + :label="__('Squash commit message')" + input-id="squash-message-edit" + squash + > + <commit-message-dropdown + slot="header" + v-model="squashCommitMessage" + :commits="commits" /> - {{ __('Include merge request description') }} - </label> - </commit-edit> - </ul> - </commits-header> + </commit-edit> + <commit-edit + v-if="shouldShowMergeEdit" + v-model="commitMessage" + :label="__('Merge commit message')" + input-id="merge-message-edit" + > + <label slot="checkbox"> + <input + id="include-description" + type="checkbox" + @change="updateMergeCommitMessage($event.target.checked)" + /> + {{ __('Include merge request description') }} + </label> + </commit-edit> + </ul> + </commits-header> + </template> </template> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue index 1c9909e7178..12fdfe601a4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue @@ -46,7 +46,7 @@ export default { name="squash" class="qa-squash-checkbox js-squash-checkbox gl-mb-0 gl-mr-2" :title="tooltipTitle" - @change="checked => $emit('input', checked)" + @change="(checked) => $emit('input', checked)" > {{ $options.i18n.checkboxLabel }} </gl-form-checkbox> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue index 1d591168a17..3f1db815f95 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue @@ -29,7 +29,7 @@ export default { variables() { return this.mergeRequestQueryVariables; }, - update: data => data.project.mergeRequest.userPermissions, + update: (data) => data.project.mergeRequest.userPermissions, }, }, props: { @@ -86,7 +86,7 @@ export default { variables: mergeRequestQueryVariables, }); - const data = produce(sourceData, draftState => { + const data = produce(sourceData, (draftState) => { // eslint-disable-next-line no-param-reassign draftState.project.mergeRequest.workInProgress = workInProgress; // eslint-disable-next-line no-param-reassign @@ -113,10 +113,18 @@ export default { }, }, }) - .then(({ data: { mergeRequestSetWip: { mergeRequest: { title } } } }) => { - createFlash(__('The merge request can now be merged.'), 'notice'); - $('.merge-request .detail-page-description .title').text(title); - }) + .then( + ({ + data: { + mergeRequestSetWip: { + mergeRequest: { title }, + }, + }, + }) => { + createFlash(__('The merge request can now be merged.'), 'notice'); + $('.merge-request .detail-page-description .title').text(title); + }, + ) .catch(() => createFlash(__('Something went wrong. Please try again.'))) .finally(() => { this.isMakingRequest = false; @@ -129,8 +137,8 @@ export default { this.isMakingRequest = true; this.service .removeWIP() - .then(res => res.data) - .then(data => { + .then((res) => res.data) + .then((data) => { eventHub.$emit('UpdateWidgetData', data); MergeRequest.toggleDraftStatus(this.mr.title, true); }) diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue index 4de41dd5887..180db7828a8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue @@ -40,7 +40,7 @@ export default { ); }, numberOfInvalidPlans() { - return Object.values(this.plansObject).filter(plan => plan.tf_report_error).length; + return Object.values(this.plansObject).filter((plan) => plan.tf_report_error).length; }, numberOfPlans() { return Object.keys(this.plansObject).length; diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js index 2d21ced1b28..6c6f5e7fc73 100644 --- a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js @@ -38,7 +38,7 @@ export default { .query({ query: issuesQuery, variables: { projectPath: targetProjectFullPath } }) .then(({ data }) => { // Return some transformed data to be rendered in the expanded state - return data.project.issues.nodes.map(issue => ({ + return data.project.issues.nodes.map((issue) => ({ id: issue.id, // Required: The ID of the object text: issue.title, // Required: The text to get used on each row // Icon to get rendered on the side of each row diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js index 8f2cca3309a..d512877a20d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/index.js @@ -26,7 +26,11 @@ export default () => { registerExtension(issueExtension); - const vm = new Vue({ ...MrWidgetOptions, apolloProvider }); + const vm = new Vue({ + el: '#js-vue-mr-widget', + ...MrWidgetOptions, + apolloProvider, + }); window.gl.mrWidget = { checkStatus: vm.checkStatus, diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js b/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js index e50555ca875..943011949fd 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js +++ b/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js @@ -11,7 +11,7 @@ export default { } }, refreshApprovals() { - return this.service.fetchApprovals().then(data => { + return this.service.fetchApprovals().then((data) => { this.mr.setApprovals(data); }); }, diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js index dc8a6b56d58..fe512d68ea2 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js @@ -27,7 +27,7 @@ export default { return __('Merge when pipeline succeeds'); }, shouldShowMergeImmediatelyDropdown() { - return this.mr.isPipelineActive && !this.mr.onlyAllowMergeIfPipelineSucceeds; + return this.isPipelineActive && !this.stateData.onlyAllowMergeIfPipelineSucceeds; }, isMergeImmediatelyDangerous() { return false; diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 3f1f2144d8e..519576d9fe6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -43,12 +43,11 @@ import SourceBranchRemovalStatus from './components/source_branch_removal_status import TerraformPlan from './components/terraform/mr_widget_terraform_container.vue'; import GroupedCodequalityReportsApp from '../reports/codequality_report/grouped_codequality_reports_app.vue'; import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue'; -import { setFaviconOverlay } from '../lib/utils/common_utils'; +import { setFaviconOverlay } from '../lib/utils/favicon'; import GroupedAccessibilityReportsApp from '../reports/accessibility_report/grouped_accessibility_reports_app.vue'; import getStateQuery from './queries/get_state.query.graphql'; export default { - el: '#js-vue-mr-widget', // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25 // eslint-disable-next-line @gitlab/require-i18n-strings name: 'MRWidget', @@ -166,7 +165,8 @@ export default { return ( !this.mr.canRemoveSourceBranch && this.mr.shouldRemoveSourceBranch && - (!this.mr.isNothingToMergeState && !this.mr.isMergedState) + !this.mr.isNothingToMergeState && + !this.mr.isMergedState ); }, shouldRenderCollaborationStatus() { @@ -190,9 +190,13 @@ export default { mergeError = mergeError.slice(0, -1); } - return sprintf(s__('mrWidget|Merge failed: %{mergeError}. Please try again.'), { - mergeError, - }); + return sprintf( + s__('mrWidget|Merge failed: %{mergeError}. Please try again.'), + { + mergeError, + }, + false, + ); }, shouldShowAccessibilityReport() { return this.mr.accessibilityReportPath; @@ -306,8 +310,7 @@ export default { callback: this.checkStatus, startingInterval: this.startingPollInterval, maxInterval: this.startingPollInterval + secondsToMilliseconds(4 * 60), - hiddenInterval: - window.gon?.features?.widgetVisibilityPolling && secondsToMilliseconds(6 * 60), + hiddenInterval: secondsToMilliseconds(6 * 60), incrementByFactorOf: 2, }); }, @@ -357,7 +360,7 @@ export default { fetchActionsContent() { this.service .fetchMergeActionsContent() - .then(res => { + .then((res) => { if (res.data) { const el = document.createElement('div'); el.innerHTML = res.data; @@ -387,26 +390,26 @@ export default { this.pollingInterval.stopTimer(); }, bindEventHubListeners() { - eventHub.$on('MRWidgetUpdateRequested', cb => { + eventHub.$on('MRWidgetUpdateRequested', (cb) => { this.checkStatus(cb); }); - eventHub.$on('MRWidgetRebaseSuccess', cb => { + eventHub.$on('MRWidgetRebaseSuccess', (cb) => { this.checkStatus(cb, true); }); // `params` should be an Array contains a Boolean, like `[true]` // Passing parameter as Boolean didn't work. - eventHub.$on('SetBranchRemoveFlag', params => { + eventHub.$on('SetBranchRemoveFlag', (params) => { [this.mr.isRemovingSourceBranch] = params; }); - eventHub.$on('FailedToMerge', mergeError => { + eventHub.$on('FailedToMerge', (mergeError) => { this.mr.state = 'failedToMerge'; this.mr.mergeError = mergeError; }); - eventHub.$on('UpdateWidgetData', data => { + eventHub.$on('UpdateWidgetData', (data) => { this.mr.setData(data); }); diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql new file mode 100644 index 00000000000..64cd70fcf42 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql @@ -0,0 +1,15 @@ +fragment autoMergeEnabled on MergeRequest { + autoMergeStrategy + mergeUser { + name + username + webUrl + avatarUrl + } + targetBranch + shouldRemoveSourceBranch + forceRemoveSourceBranch + userPermissions { + removeSourceBranch + } +} diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql new file mode 100644 index 00000000000..bdcb7a8206b --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql @@ -0,0 +1,10 @@ +#import "./auto_merge_enabled.fragment.graphql" + +query autoMergeEnabledQuery($projectPath: ID!, $iid: String!) { + project(fullPath: $projectPath) { + mergeRequest(iid: $iid) { + ...autoMergeEnabled + mergeTrainsCount + } + } +} diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_failed.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_failed.query.graphql new file mode 100644 index 00000000000..2fe0d174b67 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/auto_merge_failed.query.graphql @@ -0,0 +1,7 @@ +query autoMergeFailedQuery($projectPath: ID!, $iid: String!) { + project(fullPath: $projectPath) { + mergeRequest(iid: $iid) { + mergeError + } + } +} diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql new file mode 100644 index 00000000000..9479ef3cf79 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql @@ -0,0 +1,41 @@ +fragment ReadyToMerge on Project { + onlyAllowMergeIfPipelineSucceeds + mergeRequestsFfOnlyEnabled + squashReadOnly + mergeRequest(iid: $iid) { + autoMergeEnabled + shouldRemoveSourceBranch + defaultMergeCommitMessage + defaultMergeCommitMessageWithDescription + defaultSquashCommitMessage + squash + squashOnMerge + availableAutoMergeStrategies + hasCi + mergeable + mergeWhenPipelineSucceeds + commitCount + diffHeadSha + userPermissions { + removeSourceBranch + } + targetBranch + mergeError + commitsWithoutMergeCommits { + nodes { + sha + shortId + title + message + } + } + pipelines(first: 1) { + nodes { + id + status + path + active + } + } + } +} diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql new file mode 100644 index 00000000000..78259e1f553 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql @@ -0,0 +1,7 @@ +#import "./ready_to_merge.fragment.graphql" + +query readyToMergeQuery($projectPath: ID!, $iid: String!) { + project(fullPath: $projectPath) { + ...ReadyToMerge + } +} diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/rebase.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/rebase.query.graphql new file mode 100644 index 00000000000..a8c7d2610bf --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/rebase.query.graphql @@ -0,0 +1,11 @@ +query rebaseQuery($projectPath: ID!, $iid: String!) { + project(fullPath: $projectPath) { + mergeRequest(iid: $iid) { + rebaseInProgress + targetBranch + userPermissions { + pushToSourceBranch + } + } + } +} diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js index 2ad15f231bb..7dcb4881e7f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js +++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js @@ -60,15 +60,15 @@ export default class MRWidgetService { } fetchApprovals() { - return axios.get(this.apiApprovalsPath).then(res => res.data); + return axios.get(this.apiApprovalsPath).then((res) => res.data); } approveMergeRequest() { - return axios.post(this.apiApprovePath).then(res => res.data); + return axios.post(this.apiApprovePath).then((res) => res.data); } unapproveMergeRequest() { - return axios.post(this.apiUnapprovePath).then(res => res.data); + return axios.post(this.apiUnapprovePath).then((res) => res.data); } static executeInlineAction(url) { diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js index 15d67ea18ea..7740147c8e4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js @@ -1,6 +1,6 @@ import { s__, n__ } from '~/locale'; -export const title = state => { +export const title = (state) => { if (state.isLoading) { return s__('BuildArtifacts|Loading artifacts'); } diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/index.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/index.js index f8abbc99f0f..e6cb5ead089 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/index.js @@ -7,10 +7,11 @@ import state from './state'; Vue.use(Vuex); -export default () => - new Vuex.Store({ - actions, - mutations, - getters, - state: state(), - }); +export const getStoreConfig = () => ({ + actions, + mutations, + getters, + state: state(), +}); + +export default () => new Vuex.Store(getStoreConfig()); diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index f50b6caf0f5..a6bbab47a06 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -1,5 +1,6 @@ import { format } from 'timeago.js'; import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key'; +import mrEventHub from '~/merge_request/eventhub'; import { stateKey } from './state_maps'; import { formatDate } from '../../lib/utils/datetime_utility'; import { MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY, MWPS_MERGE_STRATEGY } from '../constants'; @@ -154,6 +155,10 @@ export default class MergeRequestStore { this.canRevertInCurrentMR = currentUser.can_revert_on_current_merge_request || false; this.setState(); + + mrEventHub.$emit('mr.state.updated', { + state: this.mergeRequestState, + }); } setGraphqlData(project) { @@ -167,7 +172,7 @@ export default class MergeRequestStore { this.canBeMerged = mergeRequest.mergeStatus === 'can_be_merged'; this.canMerge = mergeRequest.userPermissions.canMerge; this.ciStatus = pipeline?.status.toLowerCase(); - this.commitsCount = mergeRequest.commitCount; + this.commitsCount = mergeRequest.commitCount || 10; this.branchMissing = !mergeRequest.sourceBranchExists || !mergeRequest.targetBranchExists; this.hasConflicts = mergeRequest.conflicts; this.hasMergeableDiscussionsState = mergeRequest.mergeableDiscussionsState === false; diff --git a/app/assets/javascripts/vue_shared/components/actions_button.vue b/app/assets/javascripts/vue_shared/components/actions_button.vue index cb4c5f20377..2dc2c27f7ea 100644 --- a/app/assets/javascripts/vue_shared/components/actions_button.vue +++ b/app/assets/javascripts/vue_shared/components/actions_button.vue @@ -43,7 +43,7 @@ export default { return this.actions.length > 1; }, selectedAction() { - return this.actions.find(x => x.key === this.selectedKey) || this.actions[0]; + return this.actions.find((x) => x.key === this.selectedKey) || this.actions[0]; }, }, methods: { diff --git a/app/assets/javascripts/vue_shared/components/alert_details_table.vue b/app/assets/javascripts/vue_shared/components/alert_details_table.vue index 3e2b4cd35ab..655b867574d 100644 --- a/app/assets/javascripts/vue_shared/components/alert_details_table.vue +++ b/app/assets/javascripts/vue_shared/components/alert_details_table.vue @@ -49,7 +49,8 @@ export default { label: s__('AlertManagement|Key'), thClass, tdClass, - formatter: string => capitalizeFirstCharacter(convertToSentenceCase(splitCamelCase(string))), + formatter: (string) => + capitalizeFirstCharacter(convertToSentenceCase(splitCamelCase(string))), }, { key: 'value', diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue index 9a6433963bc..c1da2b8c305 100644 --- a/app/assets/javascripts/vue_shared/components/awards_list.vue +++ b/app/assets/javascripts/vue_shared/components/awards_list.vue @@ -48,7 +48,7 @@ export default { groupedAwards() { const { thumbsup, thumbsdown, ...rest } = { ...this.groupedDefaultAwards, - ...groupBy(this.awards, x => x.name), + ...groupBy(this.awards, (x) => x.name), }; return [ @@ -73,7 +73,7 @@ export default { return false; } - return awardList.some(award => award.user.id === this.currentUserId); + return awardList.some((award) => award.user.id === this.currentUserId); }, createAwardList(name, list) { return { @@ -95,11 +95,11 @@ export default { // Filter myself from list if I am awarded. if (hasReactionByCurrentUser) { - awardList = awardList.filter(award => award.user.id !== this.currentUserId); + awardList = awardList.filter((award) => award.user.id !== this.currentUserId); } // Get only 9-10 usernames to show in tooltip text. - const namesToShow = awardList.slice(0, TOOLTIP_NAME_COUNT).map(award => award.user.name); + const namesToShow = awardList.slice(0, TOOLTIP_NAME_COUNT).map((award) => award.user.name); // Get the remaining list to use in `and x more` text. const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length); diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue index f28e49df56e..14e99977a85 100644 --- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue @@ -79,6 +79,8 @@ export default { :title="tooltipTitle" :class="{ 'ml-auto': isCentered }" class="file-changed-icon d-inline-block" + data-qa-selector="changed_file_icon_content" + :data-qa-title="tooltipTitle" > <gl-icon v-if="showIcon" :name="changedIcon" :size="size" :class="changedIconClass" /> </span> diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue index e01a651806d..deca934e283 100644 --- a/app/assets/javascripts/vue_shared/components/commit.vue +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -54,7 +54,7 @@ export default { type: Object, required: false, default: undefined, - validator: ref => + validator: (ref) => ref === undefined || (Number.isFinite(ref.iid) && isString(ref.path) && !isEmpty(ref.path)), }, @@ -107,7 +107,7 @@ export default { }, computed: { /** - * Determines if we shoud render the ref info section based + * Determines if we should render the ref info section based */ shouldShowRefInfo() { return this.showRefInfo && (this.commitRef || this.mergeRequestRef); diff --git a/app/assets/javascripts/vue_shared/components/confirm_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_modal.vue index 96f800511d2..7c1d3772acd 100644 --- a/app/assets/javascripts/vue_shared/components/confirm_modal.vue +++ b/app/assets/javascripts/vue_shared/components/confirm_modal.vue @@ -30,8 +30,8 @@ export default { }; }, mounted() { - document.querySelectorAll(this.selector).forEach(button => { - button.addEventListener('click', e => { + document.querySelectorAll(this.selector).forEach((button) => { + button.addEventListener('click', (e) => { e.preventDefault(); this.path = button.dataset.path; diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue index 5ac30424f98..9ff35132ac9 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue @@ -11,7 +11,7 @@ export default { }, props: { content: { - type: String, + type: [String, ArrayBuffer], default: '', required: false, }, @@ -54,7 +54,7 @@ export default { </script> <template> - <div class="preview-container"> + <div class="preview-container" data-qa-selector="preview_container"> <image-viewer v-if="type === 'image'" :path="path" :file-size="fileSize" /> <markdown-viewer v-if="type === 'markdown'" diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue index 8d55701f499..af85a2fda06 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue @@ -39,11 +39,16 @@ export default { <div class="file-content"> <p class="gl-mt-3 file-info"> {{ fileName }} - <template v-if="fileSize > 0"> - ({{ fileSizeReadable }}) - </template> + <template v-if="fileSize > 0"> ({{ fileSizeReadable }}) </template> </p> - <a :href="path" class="btn btn-default" rel="nofollow" :download="fileName" target="_blank"> + <a + :href="path" + class="btn btn-default" + rel="nofollow" + :download="fileName" + target="_blank" + data-qa-selector="download_button" + > <gl-icon :size="16" name="download" class="float-left gl-mr-3" /> {{ __('Download') }} </a> diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue index eb7e24734ce..9ece6a52805 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue @@ -82,7 +82,7 @@ export default { </script> <template> - <div data-testid="image-viewer"> + <div data-testid="image-viewer" data-qa-selector="image_viewer_container"> <div :class="innerCssClasses" class="position-relative"> <img ref="contentImg" :src="path" @load="onImgLoad" /> <slot @@ -95,9 +95,7 @@ export default { <template v-if="hasFileSize"> {{ fileSizeReadable }} </template> - <template v-if="hasFileSize && hasDimensions"> - | - </template> + <template v-if="hasFileSize && hasDimensions"> | </template> <template v-if="hasDimensions"> <strong>{{ s__('ImageViewerDimensions|W') }}</strong >: {{ width }} | <strong>{{ s__('ImageViewerDimensions|H') }}</strong diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue index 67be76604a3..24386c90954 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue @@ -111,6 +111,6 @@ export default { <template> <div ref="markdownPreview" class="md-previewer" data-testid="md-previewer"> <gl-skeleton-loading v-if="isLoading" /> - <div v-else class="md" v-html="previewContent"></div> + <div v-else class="md gl-ml-auto gl-mr-auto" v-html="previewContent"></div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue index 79cdf308ac5..d0c2672b162 100644 --- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue +++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue @@ -202,7 +202,7 @@ export default { <template> <tooltip-on-truncate :title="timeWindowText" - :truncate-target="elem => elem.querySelector('.gl-dropdown-toggle-text')" + :truncate-target="(elem) => elem.querySelector('.gl-dropdown-toggle-text')" placement="top" class="d-inline-block" > diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue index 32a24844d71..39c1caf928e 100644 --- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue +++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue @@ -20,12 +20,12 @@ export default { state: { default: null, required: true, - validator: prop => typeof prop === 'boolean' || prop === null, + validator: (prop) => typeof prop === 'boolean' || prop === null, }, value: { default: null, required: false, - validator: prop => typeof prop === 'string' || prop === null, + validator: (prop) => typeof prop === 'string' || prop === null, }, label: { type: String, diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js index aaadc9766db..aec67a18a05 100644 --- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js +++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js @@ -25,7 +25,7 @@ export const defaultTimeRanges = [ }, ]; -export const defaultTimeRange = defaultTimeRanges.find(tr => tr.default); +export const defaultTimeRange = defaultTimeRanges.find((tr) => tr.default); export const dateFormats = { /** @@ -49,7 +49,7 @@ export const dateFormats = { * @param {string} value - Value as typed by the user * @returns true if the value can be parsed as a valid date, false otherwise */ -export const isValidInputString = value => { +export const isValidInputString = (value) => { try { // dateformat throws error that can be caught. // This is better than using `new Date()` diff --git a/app/assets/javascripts/vue_shared/components/deployment_instance.vue b/app/assets/javascripts/vue_shared/components/deployment_instance.vue new file mode 100644 index 00000000000..41b783aa011 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/deployment_instance.vue @@ -0,0 +1,91 @@ +<script> +/** + * An instance in deploy board is represented by a square in this mockup: + * https://gitlab.com/gitlab-org/gitlab-foss/uploads/2f655655c0eadf655d0ae7467b53002a/environments__deploy-graphic.png + * + * Each instance has a state and a tooltip. + * The state needs to be represented in different colors, + * see more information about this in + * https://gitlab.com/gitlab-org/gitlab/uploads/f1f00df6293d30f241dbeaa876a1e939/Screen_Shot_2019-11-26_at_3.35.43_PM.png + * + * An instance can represent a normal deploy or a canary deploy. In the latter we need to provide + * this information in the tooltip and the colors. + * Mockup is https://gitlab.com/gitlab-org/gitlab/issues/35570 + */ +import { GlLink, GlTooltipDirective } from '@gitlab/ui'; +import { mergeUrlParams } from '~/lib/utils/url_utility'; + +export default { + components: { + GlLink, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + + props: { + /** + * Represents the status of the pod. Each state is represented with a different + * color. + * It should be one of the following: + * succeeded || running || failed || pending || unknown + */ + status: { + type: String, + required: true, + default: 'succeeded', + }, + + tooltipText: { + type: String, + required: false, + default: '', + }, + + stable: { + type: Boolean, + required: false, + default: true, + }, + + podName: { + type: String, + required: false, + default: '', + }, + + logsPath: { + type: String, + required: false, + default: '', + }, + }, + + computed: { + isLink() { + return this.logsPath !== '' && this.podName !== ''; + }, + + cssClass() { + return { + [`deployment-instance-${this.status}`]: true, + 'deployment-instance-canary': !this.stable, + link: this.isLink, + }; + }, + + computedLogPath() { + return this.isLink ? mergeUrlParams({ pod_name: this.podName }, this.logsPath) : null; + }, + }, +}; +</script> +<template> + <gl-link + v-gl-tooltip + :class="cssClass" + :title="tooltipText" + :href="computedLogPath" + class="deployment-instance d-flex justify-content-center align-items-center" + /> +</template> diff --git a/app/assets/javascripts/vue_shared/components/deprecated_modal_2.vue b/app/assets/javascripts/vue_shared/components/deprecated_modal_2.vue deleted file mode 100644 index c4bce860ae4..00000000000 --- a/app/assets/javascripts/vue_shared/components/deprecated_modal_2.vue +++ /dev/null @@ -1,121 +0,0 @@ -<script> -import $ from 'jquery'; -import { GlButton } from '@gitlab/ui'; - -const buttonVariants = ['danger', 'primary', 'success', 'warning']; -const sizeVariants = ['sm', 'md', 'lg', 'xl']; - -export default { - name: 'DeprecatedModal2', // use GlModal instead - - components: { - GlButton, - }, - props: { - id: { - type: String, - required: false, - default: null, - }, - modalSize: { - type: String, - required: false, - default: 'md', - validator: value => sizeVariants.includes(value), - }, - headerTitleText: { - type: String, - required: false, - default: '', - }, - footerPrimaryButtonVariant: { - type: String, - required: false, - default: 'primary', - validator: value => buttonVariants.includes(value), - }, - footerPrimaryButtonText: { - type: String, - required: false, - default: '', - }, - }, - computed: { - modalSizeClass() { - return this.modalSize === 'md' ? '' : `modal-${this.modalSize}`; - }, - }, - mounted() { - $(this.$el) - .on('shown.bs.modal', this.opened) - .on('hidden.bs.modal', this.closed); - }, - beforeDestroy() { - $(this.$el) - .off('shown.bs.modal', this.opened) - .off('hidden.bs.modal', this.closed); - }, - methods: { - emitCancel(event) { - this.$emit('cancel', event); - }, - emitSubmit(event) { - this.$emit('submit', event); - }, - opened() { - this.$emit('open'); - }, - closed() { - this.$emit('closed'); - }, - }, -}; -</script> - -<template> - <div :id="id" class="modal fade" tabindex="-1" role="dialog"> - <div :class="modalSizeClass" class="modal-dialog" role="document"> - <div class="modal-content"> - <div class="modal-header gl-pr-4"> - <slot name="header"> - <h4 class="modal-title"> - <slot name="title"> {{ headerTitleText }} </slot> - </h4> - <gl-button - :aria-label="s__('Modal|Close')" - variant="default" - category="tertiary" - size="small" - icon="close" - class="js-modal-close-action" - data-dismiss="modal" - @click="emitCancel($event)" - /> - </slot> - </div> - - <div class="modal-body"><slot></slot></div> - - <div class="modal-footer"> - <slot name="footer"> - <gl-button - class="js-modal-cancel-action qa-modal-cancel-button" - data-dismiss="modal" - @click="emitCancel($event)" - > - {{ s__('Modal|Cancel') }} - </gl-button> - <gl-button - :class="`btn-${footerPrimaryButtonVariant}`" - class="js-modal-primary-action qa-modal-primary-button" - data-dismiss="modal" - @click="emitSubmit($event)" - > - {{ footerPrimaryButtonText }} - </gl-button> - </slot> - </div> - </div> - </div> - </div> -</template> diff --git a/app/assets/javascripts/vue_shared/components/dismissible_container.vue b/app/assets/javascripts/vue_shared/components/dismissible_container.vue index 6d5fd065751..2eaf833a6be 100644 --- a/app/assets/javascripts/vue_shared/components/dismissible_container.vue +++ b/app/assets/javascripts/vue_shared/components/dismissible_container.vue @@ -22,7 +22,7 @@ export default { .post(this.path, { feature_name: this.featureId, }) - .catch(e => { + .catch((e) => { // eslint-disable-next-line @gitlab/require-i18n-strings, no-console console.error('Failed to dismiss message.', e); }); diff --git a/app/assets/javascripts/vue_shared/components/editor_lite.vue b/app/assets/javascripts/vue_shared/components/editor_lite.vue index cfe3ce0a11c..7218b84cf8a 100644 --- a/app/assets/javascripts/vue_shared/components/editor_lite.vue +++ b/app/assets/javascripts/vue_shared/components/editor_lite.vue @@ -84,6 +84,9 @@ export default { onFileChange() { this.$emit('input', this.editor.getValue()); }, + getEditor() { + return this.editor; + }, }, }; </script> diff --git a/app/assets/javascripts/vue_shared/components/file_finder/index.vue b/app/assets/javascripts/vue_shared/components/file_finder/index.vue index 05403b38850..27933f87929 100644 --- a/app/assets/javascripts/vue_shared/components/file_finder/index.vue +++ b/app/assets/javascripts/vue_shared/components/file_finder/index.vue @@ -128,7 +128,7 @@ export default { this.focusedIndex = 0; } - Mousetrap.bind(['t', 'mod+p'], e => { + Mousetrap.bind(['t', 'mod+p'], (e) => { if (e.preventDefault) { e.preventDefault(); } diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js index 91a0ac3aa92..f7cfb59be01 100644 --- a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js +++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js @@ -593,15 +593,6 @@ const fileNameIcons = { export default function getIconForFile(name) { return ( - fileNameIcons[name] || - fileExtensionIcons[ - name - ? name - .split('.') - .pop() - .toLowerCase() - : '' - ] || - '' + fileNameIcons[name] || fileExtensionIcons[name ? name.split('.').pop().toLowerCase() : ''] || '' ); } diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue index 4d07d9fcfdd..96567111bbc 100644 --- a/app/assets/javascripts/vue_shared/components/file_row.vue +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -137,7 +137,11 @@ export default { @click="clickFile" @mouseleave="$emit('mouseleave', $event)" > - <div class="file-row-name-container"> + <div + class="file-row-name-container" + data-qa-selector="file_row_container" + :data-qa-file-name="file.name" + > <span ref="textOutput" :style="levelIndentation" diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue index 3988b3814f9..a4c5ca28494 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue @@ -59,7 +59,7 @@ export default { type: String, required: false, default: '', - validator: value => value === '' || /(_desc)|(_asc)/g.test(value), + validator: (value) => value === '' || /(_desc)|(_asc)/g.test(value), }, showCheckbox: { type: Boolean, @@ -89,7 +89,7 @@ export default { if (this.initialSortBy) { selectedSortOption = this.sortOptions .filter( - sortBy => + (sortBy) => sortBy.sortDirection.ascending === this.initialSortBy || sortBy.sortDirection.descending === this.initialSortBy, ) @@ -204,12 +204,12 @@ export default { this.recentSearchesStore = new RecentSearchesStore({ isLocalStorageAvailable: RecentSearchesService.isAvailable(), - allowedKeys: this.tokens.map(token => token.type), + allowedKeys: this.tokens.map((token) => token.type), }); this.recentSearchesPromise = this.recentSearchesService .fetch() - .catch(error => { + .catch((error) => { if (error.name === 'RecentSearchesServiceError') return undefined; createFlash(__('An error occurred while parsing recent searches')); @@ -217,7 +217,7 @@ export default { // Gracefully fail to empty array return []; }) - .then(searches => { + .then((searches) => { if (!searches) return; // Put any searches that may have come in before @@ -250,13 +250,13 @@ export default { * spaces. */ removeQuotesEnclosure(filters = []) { - return filters.map(filter => { + return filters.map((filter) => { if (typeof filter === 'object') { const valueString = filter.value.data; return { ...filter, value: { - data: stripQuotes(valueString), + data: typeof valueString === 'string' ? stripQuotes(valueString) : valueString, operator: filter.value.operator, }, }; @@ -305,8 +305,8 @@ export default { }, historyTokenOptionTitle(historyToken) { const tokenOption = this.tokens - .find(token => token.type === historyToken.type) - ?.options?.find(option => option.value === historyToken.value.data); + .find((token) => token.type === historyToken.type) + ?.options?.find((option) => option.value === historyToken.value.data); if (!tokenOption?.title) { return historyToken.value.data; diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js index e7d7b7d9f1b..a15cf220ee5 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js @@ -8,7 +8,7 @@ import { queryToObject } from '~/lib/utils/url_utility'; * * @returns {String} String without any enclosure */ -export const stripQuotes = value => value.replace(/^('|")(.*)('|")$/, '$2'); +export const stripQuotes = (value) => value.replace(/^('|")(.*)('|")$/, '$2'); /** * This method removes duplicate tokens from tokens array. @@ -17,7 +17,7 @@ export const stripQuotes = value => value.replace(/^('|")(.*)('|")$/, '$2'); * * @returns {Array} Unique array of tokens */ -export const uniqueTokens = tokens => { +export const uniqueTokens = (tokens) => { const knownTokens = []; return tokens.reduce((uniques, token) => { if (typeof token === 'object' && token.type !== 'filtered-search-term') { @@ -61,7 +61,7 @@ export function prepareTokens(filters = {}) { return memo; } if (Array.isArray(value)) { - return [...memo, ...value.map(filterValue => createToken(key, filterValue))]; + return [...memo, ...value.map((filterValue) => createToken(key, filterValue))]; } return [...memo, createToken(key, value)]; @@ -99,8 +99,8 @@ export function filterToQueryObject(filters = {}) { let selected; let unselected; if (Array.isArray(filter)) { - selected = filter.filter(item => item.operator === '=').map(item => item.value); - unselected = filter.filter(item => item.operator === '!=').map(item => item.value); + selected = filter.filter((item) => item.operator === '=').map((item) => item.value); + unselected = filter.filter((item) => item.operator === '!=').map((item) => item.value); } else { selected = filter?.operator === '=' ? filter.value : null; unselected = filter?.operator === '!=' ? filter.value : null; @@ -155,7 +155,7 @@ export function urlQueryToFilter(query = '') { previousValues = memo[filterName]; } if (Array.isArray(value)) { - const newAdditions = value.filter(Boolean).map(item => ({ value: item, operator })); + const newAdditions = value.filter(Boolean).map((item) => ({ value: item, operator })); return { ...memo, [filterName]: [...previousValues, ...newAdditions] }; } diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js index 443cb28cf10..411654d15f4 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js @@ -17,7 +17,7 @@ export function fetchBranches({ commit, state }, search = '') { commit(types.REQUEST_BRANCHES); return Api.branches(projectEndpoint, search) - .then(response => { + .then((response) => { commit(types.RECEIVE_BRANCHES_SUCCESS, response.data); return response; }) @@ -34,7 +34,7 @@ export const fetchMilestones = ({ commit, state }, search_title = '') => { return axios .get(milestonesEndpoint, { params: { search_title } }) - .then(response => { + .then((response) => { commit(types.RECEIVE_MILESTONES_SUCCESS, response.data); return response; }) @@ -50,7 +50,7 @@ export const fetchLabels = ({ commit, state }, search = '') => { return axios .get(state.labelsEndpoint, { params: { search } }) - .then(response => { + .then((response) => { commit(types.RECEIVE_LABELS_SUCCESS, response.data); return response; }) @@ -67,13 +67,13 @@ function fetchUser(options = {}) { let fetchUserPromise; if (projectEndpoint) { - fetchUserPromise = Api.projectUsers(projectEndpoint, query).then(data => ({ data })); + fetchUserPromise = Api.projectUsers(projectEndpoint, query).then((data) => ({ data })); } else { fetchUserPromise = Api.groupMembers(groupEndpoint, { query }); } return fetchUserPromise - .then(response => { + .then((response) => { commit(`RECEIVE_${action}_SUCCESS`, response.data); return response; }) diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue index ee0e00b0f5d..d53c829a48e 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue @@ -43,7 +43,7 @@ export default { return this.value.data.toLowerCase(); }, activeAuthor() { - return this.authors.find(author => author.username.toLowerCase() === this.currentValue); + return this.authors.find((author) => author.username.toLowerCase() === this.currentValue); }, }, watch: { @@ -63,7 +63,7 @@ export default { : this.config.fetchAuthors(searchTerm); fetchPromise - .then(res => { + .then((res) => { // We'd want to avoid doing this check but // users.json and /groups/:id/members & /projects/:id/users // return response differently. diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue index c18bdfc5c20..694dcd95b5e 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue @@ -43,7 +43,7 @@ export default { return this.value.data.toLowerCase(); }, activeBranch() { - return this.branches.find(branch => branch.name.toLowerCase() === this.currentValue); + return this.branches.find((branch) => branch.name.toLowerCase() === this.currentValue); }, }, watch: { diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue index 7a9c5c277eb..d59e9200e6c 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue @@ -47,7 +47,7 @@ export default { }, activeLabel() { return this.labels.find( - label => label.title.toLowerCase() === stripQuotes(this.currentValue), + (label) => label.title.toLowerCase() === stripQuotes(this.currentValue), ); }, containerStyle() { @@ -74,7 +74,7 @@ export default { this.loading = true; this.config .fetchLabels(searchTerm) - .then(res => { + .then((res) => { // We'd want to avoid doing this check but // labels.json and /groups/:id/labels & /projects/:id/labels // return response differently. diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue index c24df5e081d..0dd7820073a 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue @@ -43,7 +43,7 @@ export default { }, activeMilestone() { return this.milestones.find( - milestone => milestone.title.toLowerCase() === stripQuotes(this.currentValue), + (milestone) => milestone.title.toLowerCase() === stripQuotes(this.currentValue), ); }, }, diff --git a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue index 1ad0ca36bf8..9ab91e567e6 100644 --- a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue +++ b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue @@ -4,6 +4,7 @@ import { GfmAutocompleteType, tributeConfig, } from 'ee_else_ce/vue_shared/components/gfm_autocomplete/utils'; +import * as Emoji from '~/emoji'; import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; @@ -27,7 +28,7 @@ export default { }, computed: { config() { - return this.autocompleteTypes.map(type => ({ + return this.autocompleteTypes.map((type) => ({ ...tributeConfig[type].config, loadingItemTemplate: `<span class="gl-spinner gl-vertical-align-text-bottom gl-ml-3 gl-mr-2"></span>${__( 'Loading', @@ -55,7 +56,7 @@ export default { if (!this.assignees || !isAssigneesLengthSame) { this.assignees = - SidebarMediator.singleton?.store?.assignees?.map(assignee => assignee.username) || []; + SidebarMediator.singleton?.store?.assignees?.map((assignee) => assignee.username) || []; } }, filterValues(type) { @@ -76,10 +77,18 @@ export default { return (inputText, processValues) => { if (this.cache[type]) { processValues(this.filterValues(type)); + } else if (type === GfmAutocompleteType.Emojis) { + Emoji.initEmojiMap() + .then(() => { + const emojis = Emoji.getValidEmojiNames(); + this.cache[type] = emojis; + processValues(emojis); + }) + .catch(() => createFlash({ message: this.$options.errorMessage })); } else if (this.dataSources[type]) { axios .get(this.dataSources[type]) - .then(response => { + .then((response) => { this.cache[type] = response.data; processValues(this.filterValues(type)); }) diff --git a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js index 2581888b504..809932b0f29 100644 --- a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js +++ b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js @@ -1,16 +1,24 @@ import { escape, last } from 'lodash'; +import * as Emoji from '~/emoji'; import { spriteIcon } from '~/lib/utils/common_utils'; const groupType = 'Group'; // eslint-disable-line @gitlab/require-i18n-strings +// Number of users to show in the autocomplete menu to avoid doing a mass fetch of 100+ avatars +const memberLimit = 10; + const nonWordOrInteger = /\W|^\d+$/; +export const menuItemLimit = 100; + export const GfmAutocompleteType = { + Emojis: 'emojis', Issues: 'issues', Labels: 'labels', Members: 'members', MergeRequests: 'mergeRequests', Milestones: 'milestones', + QuickActions: 'commands', Snippets: 'snippets', }; @@ -21,10 +29,21 @@ function doesCurrentLineStartWith(searchString, fullText, selectionStart) { } export const tributeConfig = { + [GfmAutocompleteType.Emojis]: { + config: { + trigger: ':', + lookup: (value) => value, + menuItemLimit, + menuItemTemplate: ({ original }) => `${original} ${Emoji.glEmojiTag(original)}`, + selectTemplate: ({ original }) => `:${original}:`, + }, + }, + [GfmAutocompleteType.Issues]: { config: { trigger: '#', - lookup: value => `${value.iid}${value.title}`, + lookup: (value) => `${value.iid}${value.title}`, + menuItemLimit, menuItemTemplate: ({ original }) => `<small>${original.reference || original.iid}</small> ${escape(original.title)}`, selectTemplate: ({ original }) => original.reference || `#${original.iid}`, @@ -35,6 +54,7 @@ export const tributeConfig = { config: { trigger: '~', lookup: 'title', + menuItemLimit, menuItemTemplate: ({ original }) => ` <span class="dropdown-label-box" style="background: ${escape(original.color)};"></span> ${escape(original.title)}`, @@ -45,11 +65,11 @@ export const tributeConfig = { }, filterValues({ collection, fullText, selectionStart }) { if (doesCurrentLineStartWith('/label', fullText, selectionStart)) { - return collection.filter(label => !label.set); + return collection.filter((label) => !label.set); } if (doesCurrentLineStartWith('/unlabel', fullText, selectionStart)) { - return collection.filter(label => label.set); + return collection.filter((label) => label.set); } return collection; @@ -60,8 +80,9 @@ export const tributeConfig = { config: { trigger: '@', fillAttr: 'username', - lookup: value => + lookup: (value) => value.type === groupType ? last(value.name.split(' / ')) : `${value.name}${value.username}`, + menuItemLimit: memberLimit, menuItemTemplate: ({ original }) => { const commonClasses = 'gl-avatar gl-avatar-s24 gl-flex-shrink-0'; const noAvatarClasses = `${commonClasses} gl-rounded-small @@ -101,11 +122,11 @@ export const tributeConfig = { }, filterValues({ assignees, collection, fullText, selectionStart }) { if (doesCurrentLineStartWith('/assign', fullText, selectionStart)) { - return collection.filter(member => !assignees.includes(member.username)); + return collection.filter((member) => !assignees.includes(member.username)); } if (doesCurrentLineStartWith('/unassign', fullText, selectionStart)) { - return collection.filter(member => assignees.includes(member.username)); + return collection.filter((member) => assignees.includes(member.username)); } return collection; @@ -115,7 +136,8 @@ export const tributeConfig = { [GfmAutocompleteType.MergeRequests]: { config: { trigger: '!', - lookup: value => `${value.iid}${value.title}`, + lookup: (value) => `${value.iid}${value.title}`, + menuItemLimit, menuItemTemplate: ({ original }) => `<small>${original.reference || original.iid}</small> ${escape(original.title)}`, selectTemplate: ({ original }) => original.reference || `!${original.iid}`, @@ -126,16 +148,47 @@ export const tributeConfig = { config: { trigger: '%', lookup: 'title', + menuItemLimit, menuItemTemplate: ({ original }) => escape(original.title), selectTemplate: ({ original }) => `%"${escape(original.title)}"`, }, }, + [GfmAutocompleteType.QuickActions]: { + config: { + trigger: '/', + fillAttr: 'name', + lookup: (value) => `${value.name}${value.aliases.join()}`, + menuItemLimit, + menuItemTemplate: ({ original }) => { + const aliases = original.aliases.length + ? `<small>(or /${original.aliases.join(', /')})</small>` + : ''; + + const params = original.params.length ? `<small>${original.params.join(' ')}</small>` : ''; + + let description = ''; + + if (original.warning) { + const confidentialIcon = + original.icon === 'confidential' ? spriteIcon('eye-slash', 's16 gl-mr-2') : ''; + description = `<small>${confidentialIcon}<em>${original.warning}</em></small>`; + } else if (original.description) { + description = `<small><em>${original.description}</em></small>`; + } + + return `<div>/${original.name} ${aliases} ${params}</div> + <div>${description}</div>`; + }, + }, + }, + [GfmAutocompleteType.Snippets]: { config: { trigger: '$', fillAttr: 'id', - lookup: value => `${value.id}${value.title}`, + lookup: (value) => `${value.id}${value.title}`, + menuItemLimit, menuItemTemplate: ({ original }) => `<small>${original.id}</small> ${escape(original.title)}`, }, }, diff --git a/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js b/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js index 221c4f5b8a8..0a6b50674f0 100644 --- a/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js +++ b/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js @@ -15,5 +15,5 @@ function cleanSuggestionLine(line = {}) { } export function selectDiffLines(lines) { - return lines.filter(line => line.type !== 'match').map(line => cleanSuggestionLine(line)); + return lines.filter((line) => line.type !== 'match').map((line) => cleanSuggestionLine(line)); } diff --git a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue index b9729a3dc5c..10887aee689 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue @@ -1,6 +1,5 @@ <script> import { GlDropdown, GlDropdownForm, GlFormTextarea, GlButton } from '@gitlab/ui'; -import { __, sprintf } from '~/locale'; export default { components: { GlDropdown, GlDropdownForm, GlFormTextarea, GlButton }, @@ -10,7 +9,7 @@ export default { required: false, default: false, }, - fileName: { + defaultCommitMessage: { type: String, required: true, }, @@ -18,18 +17,11 @@ export default { data() { return { message: null, - buttonText: __('Apply suggestion'), - headerText: __('Apply suggestion commit message'), }; }, - computed: { - placeholderText() { - return sprintf(__('Apply suggestion on %{fileName}'), { fileName: this.fileName }); - }, - }, methods: { onApply() { - this.$emit('apply', this.message || this.placeholderText); + this.$emit('apply', this.message); }, }, }; @@ -37,18 +29,26 @@ export default { <template> <gl-dropdown - :text="buttonText" - :header-text="headerText" + :text="__('Apply suggestion')" :disabled="disabled" boundary="window" right - menu-class="gl-w-full! gl-pb-0!" + menu-class="gl-w-full!" + @shown="$refs.commitMessage.$el.focus()" > - <gl-dropdown-form class="gl-m-3!"> - <gl-form-textarea v-model="message" :placeholder="placeholderText" /> + <gl-dropdown-form class="gl-px-4! gl-m-0!"> + <label for="commit-message">{{ __('Commit message') }}</label> + <gl-form-textarea + id="commit-message" + ref="commitMessage" + v-model="message" + :placeholder="defaultCommitMessage" + submit-on-enter + @submit="onApply" + /> <gl-button - class="gl-w-quarter! gl-mt-3 gl-text-center! float-right" - category="secondary" + class="gl-w-auto! gl-mt-3 gl-text-center! gl-hover-text-white! gl-transition-medium! float-right" + category="primary" variant="success" @click="onApply" > diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 232a3054cd0..b6e167524aa 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -158,7 +158,7 @@ export default { const mediaInPreview = this.$refs['markdown-preview'].querySelectorAll('video, audio'); if (mediaInPreview) { - mediaInPreview.forEach(media => { + mediaInPreview.forEach((media) => { media.pause(); }); } @@ -169,7 +169,7 @@ export default { return new GLForm( $(this.$refs['gl-form']), { - emojis: this.enableAutocomplete, + emojis: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, members: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, issues: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, mergeRequests: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, @@ -199,7 +199,7 @@ export default { this.markdownPreview = __('Loadingā¦'); axios .post(this.markdownPreviewPath, { text: this.textareaValue }) - .then(response => this.renderMarkdown(response.data)) + .then((response) => this.renderMarkdown(response.data)) .catch(() => new Flash(__('Error loading markdown preview'))); } else { this.renderMarkdown(); diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index d0a0560846a..173d192dab0 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -110,7 +110,7 @@ export default { const area = this.$el.parentNode.querySelector('textarea'); CopyAsGFM.nodeToGFM(transformed) - .then(gfm => { + .then((gfm) => { CopyAsGFM.insertPastedText(area, documentFragment.textContent, CopyAsGFM.quoted(gfm)); }) .catch(() => {}); diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue index 13ec7a6ada9..93a270b8a97 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue @@ -27,6 +27,10 @@ export default { type: String, required: true, }, + defaultCommitMessage: { + type: String, + required: true, + }, suggestionsCount: { type: Number, required: false, @@ -47,8 +51,8 @@ export default { }, }, methods: { - applySuggestion(callback) { - this.$emit('apply', { suggestionId: this.suggestion.id, callback }); + applySuggestion(callback, message) { + this.$emit('apply', { suggestionId: this.suggestion.id, callback, message }); }, applySuggestionBatch() { this.$emit('applyBatch'); @@ -74,6 +78,7 @@ export default { :is-applying-batch="suggestion.is_applying_batch" :batch-suggestions-count="batchSuggestionsCount" :help-page-path="helpPagePath" + :default-commit-message="defaultCommitMessage" :inapplicable-reason="suggestion.inapplicable_reason" @apply="applySuggestion" @applyBatch="applySuggestionBatch" diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue index fb51840b689..63341b433e0 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue @@ -2,9 +2,10 @@ import { GlButton, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import ApplySuggestion from './apply_suggestion.vue'; export default { - components: { GlIcon, GlButton, GlLoadingIcon }, + components: { GlIcon, GlButton, GlLoadingIcon, ApplySuggestion }, directives: { 'gl-tooltip': GlTooltipDirective }, mixins: [glFeatureFlagsMixin()], props: { @@ -37,6 +38,10 @@ export default { type: String, required: true, }, + defaultCommitMessage: { + type: String, + required: true, + }, inapplicableReason: { type: String, required: false, @@ -57,6 +62,9 @@ export default { canBeBatched() { return Boolean(this.glFeatures.batchSuggestions); }, + canAddCustomCommitMessage() { + return this.glFeatures.suggestionsCustomCommit; + }, isApplying() { return this.isApplyingSingle || this.isApplyingBatch; }, @@ -77,10 +85,10 @@ export default { }, }, methods: { - applySuggestion() { + applySuggestion(message) { if (!this.canApply) return; this.isApplyingSingle = true; - this.$emit('apply', this.applySuggestionCallback); + this.$emit('apply', this.applySuggestionCallback, message); }, applySuggestionCallback() { this.isApplyingSingle = false; @@ -142,7 +150,14 @@ export default { > {{ __('Add suggestion to batch') }} </gl-button> - <span v-gl-tooltip.viewport="tooltipMessage" tabindex="0"> + <apply-suggestion + v-if="canAddCustomCommitMessage" + :disabled="isDisableButton" + :default-commit-message="defaultCommitMessage" + class="gl-ml-3" + @apply="applySuggestion" + /> + <span v-else v-gl-tooltip.viewport="tooltipMessage" tabindex="0"> <gl-button v-if="isLoggedIn" class="btn-inverted js-apply-btn btn-grouped" diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue index 927a93487e6..5ee51764555 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue @@ -38,6 +38,10 @@ export default { type: String, required: true, }, + defaultCommitMessage: { + type: String, + required: true, + }, suggestionsCount: { type: Number, required: false, @@ -82,27 +86,41 @@ export default { this.isRendered = true; }, generateDiff(suggestionIndex) { - const { suggestions, disabled, batchSuggestionsInfo, helpPagePath, suggestionsCount } = this; + const { + suggestions, + disabled, + batchSuggestionsInfo, + helpPagePath, + defaultCommitMessage, + suggestionsCount, + } = this; const suggestion = suggestions && suggestions[suggestionIndex] ? suggestions[suggestionIndex] : {}; const SuggestionDiffComponent = Vue.extend(SuggestionDiff); const suggestionDiff = new SuggestionDiffComponent({ - propsData: { disabled, suggestion, batchSuggestionsInfo, helpPagePath, suggestionsCount }, + propsData: { + disabled, + suggestion, + batchSuggestionsInfo, + helpPagePath, + defaultCommitMessage, + suggestionsCount, + }, }); - suggestionDiff.$on('apply', ({ suggestionId, callback }) => { - this.$emit('apply', { suggestionId, callback, flashContainer: this.$el }); + suggestionDiff.$on('apply', ({ suggestionId, callback, message }) => { + this.$emit('apply', { suggestionId, callback, flashContainer: this.$el, message }); }); suggestionDiff.$on('applyBatch', () => { this.$emit('applyBatch', { flashContainer: this.$el }); }); - suggestionDiff.$on('addToBatch', suggestionId => { + suggestionDiff.$on('addToBatch', (suggestionId) => { this.$emit('addToBatch', suggestionId); }); - suggestionDiff.$on('removeFromBatch', suggestionId => { + suggestionDiff.$on('removeFromBatch', (suggestionId) => { this.$emit('removeFromBatch', suggestionId); }); diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index 5824cb9438f..15c5b9d6733 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -49,10 +49,10 @@ export default { ) " > - <template #markdownDocsLink="{content}"> + <template #markdownDocsLink="{ content }"> <gl-link :href="markdownDocsPath" target="_blank">{{ content }}</gl-link> </template> - <template #quickActionsDocsLink="{content}"> + <template #quickActionsDocsLink="{ content }"> <gl-link :href="quickActionsDocsPath" target="_blank">{{ content }}</gl-link> </template> </gl-sprintf> @@ -81,7 +81,7 @@ export default { ) " > - <template #retryButton="{content}"> + <template #retryButton="{ content }"> <gl-button variant="link" category="primary" @@ -90,7 +90,7 @@ export default { {{ content }} </gl-button> </template> - <template #newFileButton="{content}"> + <template #newFileButton="{ content }"> <gl-button variant="link" category="primary" diff --git a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue index de9c84dd157..e3a7f144321 100644 --- a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue +++ b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue @@ -55,6 +55,11 @@ export default { required: false, default: null, }, + category: { + type: String, + required: false, + default: 'primary', + }, }, computed: { modalDomId() { @@ -70,14 +75,14 @@ export default { document.body, }); this.clipboard - .on('success', e => { + .on('success', (e) => { this.$root.$emit('bv::hide::tooltip', this.id); this.$emit('success', e); // Clear the selection and blur the trigger so it loses its border e.clearSelection(); e.trigger.blur(); }) - .on('error', e => this.$emit('error', e)); + .on('error', (e) => this.$emit('error', e)); }); }, destroyed() { @@ -95,6 +100,7 @@ export default { :data-clipboard-target="target" :data-clipboard-text="text" :title="title" + :category="category" icon="copy-to-clipboard" /> </template> diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue index 3749888ee36..653ee7f20e9 100644 --- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue +++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue @@ -1,5 +1,6 @@ <script> import $ from 'jquery'; +import { GlBadge, GlTabs, GlTab } from '@gitlab/ui'; /** * Given an array of tabs, renders non linked bootstrap tabs. @@ -23,6 +24,11 @@ import $ from 'jquery'; */ export default { name: 'NavigationTabs', + components: { + GlBadge, + GlTabs, + GlTab, + }, props: { tabs: { type: Array, @@ -50,24 +56,21 @@ export default { }; </script> <template> - <ul class="nav-links scrolling-tabs separator"> - <li + <gl-tabs class="gl-display-flex gl-w-full" nav-class="gl-border-0!"> + <gl-tab v-for="(tab, i) in tabs" :key="i" - :class="{ - active: tab.isActive, - }" + :title-link-class="`js-${scope}-tab-${tab.scope} gl-display-inline-flex`" + :title-link-attributes="{ 'data-testid': `${scope}-tab-${tab.scope}` }" + :active="tab.isActive" + @click="onTabClick(tab)" > - <a - :class="`js-${scope}-tab-${tab.scope}`" - :data-testid="`${scope}-tab-${tab.scope}`" - role="button" - @click="onTabClick(tab)" - > - {{ tab.name }} - - <span v-if="shouldRenderBadge(tab.count)" class="badge badge-pill"> {{ tab.count }} </span> - </a> - </li> - </ul> + <template #title> + <span class="gl-mr-2"> {{ tab.name }} </span> + <gl-badge v-if="shouldRenderBadge(tab.count)" size="sm" class="gl-tab-counter-badge">{{ + tab.count + }}</gl-badge> + </template> + </gl-tab> + </gl-tabs> </template> diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index f30676e8ef3..cc1203f83f0 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -78,16 +78,10 @@ export default { }, // following 2 methods taken from code in `collapseLongCommitList` of notes.js: actionTextHtml() { - return $(this.note.note_html) - .unwrap() - .html(); + return $(this.note.note_html).unwrap().html(); }, hasMoreCommits() { - return ( - $(this.note.note_html) - .filter('ul') - .children().length > MAX_VISIBLE_COMMIT_LIST_COUNT - ); + return $(this.note.note_html).filter('ul').children().length > MAX_VISIBLE_COMMIT_LIST_COUNT; }, descriptionVersion() { return this.descriptionVersions[this.note.description_version_id]; diff --git a/app/assets/javascripts/vue_shared/components/ordered_layout.vue b/app/assets/javascripts/vue_shared/components/ordered_layout.vue index 117e79ca39f..a57ff10de71 100644 --- a/app/assets/javascripts/vue_shared/components/ordered_layout.vue +++ b/app/assets/javascripts/vue_shared/components/ordered_layout.vue @@ -4,7 +4,7 @@ export default { render(h, context) { const { slotKeys } = context.props; const slots = context.slots(); - const children = slotKeys.map(key => slots[key]).filter(x => x); + const children = slotKeys.map((key) => slots[key]).filter((x) => x); return children; }, diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue index 1fc39c7cb8e..d03987bbbe0 100644 --- a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue +++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue @@ -203,7 +203,7 @@ export default { this.resetPagination(); const filterParams = { authorUsername: '', assigneeUsername: '', search: '' }; - filters.forEach(filter => { + filters.forEach((filter) => { if (typeof filter === 'object') { switch (filter.type) { case 'author_username': diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/utils.js b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/utils.js index 7de4263acbb..7855c7ea6cb 100644 --- a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/utils.js +++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/utils.js @@ -6,6 +6,6 @@ import { __ } from '~/locale'; * @param {String} value * @returns {String} */ -export const isAny = value => { +export const isAny = (value) => { return value === __('Any') ? '' : value; }; diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue index 85481f3f7b4..3c0ac32e512 100644 --- a/app/assets/javascripts/vue_shared/components/pikaday.vue +++ b/app/assets/javascripts/vue_shared/components/pikaday.vue @@ -1,20 +1,13 @@ <script> -import Pikaday from 'pikaday'; -import { GlIcon } from '@gitlab/ui'; -import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility'; -import { __ } from '~/locale'; +import { GlDatepicker } from '@gitlab/ui'; +import { pikadayToString } from '~/lib/utils/datetime_utility'; export default { name: 'DatePicker', components: { - GlIcon, + GlDatepicker, }, props: { - label: { - type: String, - required: false, - default: __('Date picker'), - }, selectedDate: { type: Date, required: false, @@ -31,32 +24,9 @@ export default { default: null, }, }, - mounted() { - this.calendar = new Pikaday({ - field: this.$el.querySelector('.dropdown-menu-toggle'), - theme: 'gitlab-theme animate-picker', - format: 'yyyy-mm-dd', - container: this.$el, - defaultDate: this.selectedDate, - setDefaultDate: Boolean(this.selectedDate), - minDate: this.minDate, - maxDate: this.maxDate, - parse: dateString => parsePikadayDate(dateString), - toString: date => pikadayToString(date), - onSelect: this.selected.bind(this), - onClose: this.toggled.bind(this), - firstDay: gon.first_day_of_week, - }); - - this.$el.append(this.calendar.el); - this.calendar.show(); - }, - beforeDestroy() { - this.calendar.destroy(); - }, methods: { - selected(dateText) { - this.$emit('newDateSelected', this.calendar.toString(dateText)); + selected(date) { + this.$emit('newDateSelected', pikadayToString(date)); }, toggled() { this.$emit('hidePicker'); @@ -66,12 +36,13 @@ export default { </script> <template> - <div class="pikaday-container"> - <div class="dropdown open"> - <button type="button" class="dropdown-menu-toggle" data-toggle="dropdown" @click="toggled"> - <span class="dropdown-toggle-text"> {{ label }} </span> - <gl-icon name="chevron-down" class="gl-absolute gl-right-3 gl-top-3 gl-text-gray-500" /> - </button> - </div> - </div> + <gl-datepicker + :value="selectedDate" + :min-date="minDate" + :max-date="maxDate" + start-opened + @close="toggled" + @click="toggled" + @input="selected" + /> </template> diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue index 154671fe9fa..65bd4e4382d 100644 --- a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue +++ b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue @@ -13,7 +13,7 @@ export default { project: { type: Object, required: true, - validator: p => + validator: (p) => (Number.isFinite(p.id) || isString(p.id)) && isString(p.name) && (isString(p.name_with_namespace) || isString(p.nameWithNamespace)), diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue index 4e2029cd74f..e659e2155fb 100644 --- a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue +++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue @@ -48,10 +48,14 @@ export default { data() { return { searchQuery: '', + hasSearched: false, }; }, computed: { legendText() { + if (!this.hasSearched) { + return ''; + } const count = this.projectSearchResults.length; const total = this.totalResults; @@ -75,6 +79,9 @@ export default { return this.selectedProjects.some(({ id }) => project.id === id); }, onInput: debounce(function debouncedOnInput() { + if (!this.hasSearched) { + this.hasSearched = true; + } this.$emit('searched', this.searchQuery); }, SEARCH_INPUT_TIMEOUT_MS), }, @@ -115,7 +122,7 @@ export default { </template> <template #default> - {{ legendText }} + <span data-testid="legend-text">{{ legendText }}</span> </template> </gl-infinite-scroll> <div v-if="showNoResultsMessage" class="text-muted ml-2 js-no-results-message"> diff --git a/app/assets/javascripts/vue_shared/components/registry/list_item.vue b/app/assets/javascripts/vue_shared/components/registry/list_item.vue index 7046ac5be03..8965dba3e83 100644 --- a/app/assets/javascripts/vue_shared/components/registry/list_item.vue +++ b/app/assets/javascripts/vue_shared/components/registry/list_item.vue @@ -39,7 +39,7 @@ export default { }, }, mounted() { - this.detailsSlots = Object.keys(this.$slots).filter(k => k.startsWith('details-')); + this.detailsSlots = Object.keys(this.$slots).filter((k) => k.startsWith('details-')); }, methods: { toggleDetails() { diff --git a/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue b/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue index 8ef623b68eb..93396219a54 100644 --- a/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue +++ b/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue @@ -1,5 +1,5 @@ <script> -import { GlIcon, GlLink } from '@gitlab/ui'; +import { GlIcon, GlLink, GlTooltipDirective } from '@gitlab/ui'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; export default { @@ -9,6 +9,9 @@ export default { GlLink, TooltipOnTruncate, }, + directives: { + GlTooltip: GlTooltipDirective, + }, props: { icon: { type: String, @@ -32,6 +35,11 @@ export default { return !value || ['xs', 's', 'm', 'l', 'xl'].includes(value); }, }, + textTooltip: { + type: String, + required: false, + default: '', + }, }, computed: { sizeClass() { @@ -55,9 +63,12 @@ export default { class="gl-font-weight-bold gl-display-inline-flex" :class="sizeClass" > - <tooltip-on-truncate :title="text" class="gl-text-truncate"> + <tooltip-on-truncate v-if="!textTooltip" :title="text" class="gl-text-truncate"> {{ text }} </tooltip-on-truncate> + <span v-else v-gl-tooltip="{ title: textTooltip }" data-testid="text-tooltip-container"> + {{ text }}</span + > </div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/registry/title_area.vue b/app/assets/javascripts/vue_shared/components/registry/title_area.vue index 4d47a34c9a3..c63d91b78d3 100644 --- a/app/assets/javascripts/vue_shared/components/registry/title_area.vue +++ b/app/assets/javascripts/vue_shared/components/registry/title_area.vue @@ -1,5 +1,5 @@ <script> -import { GlAvatar, GlSprintf, GlLink } from '@gitlab/ui'; +import { GlAvatar, GlSprintf, GlLink, GlSkeletonLoader } from '@gitlab/ui'; export default { name: 'TitleArea', @@ -7,6 +7,7 @@ export default { GlAvatar, GlSprintf, GlLink, + GlSkeletonLoader, }, props: { avatar: { @@ -24,6 +25,11 @@ export default { default: () => [], required: false, }, + metadataLoading: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -32,11 +38,11 @@ export default { }, async mounted() { const METADATA_PREFIX = 'metadata-'; - this.metadataSlots = Object.keys(this.$slots).filter(k => k.startsWith(METADATA_PREFIX)); + this.metadataSlots = Object.keys(this.$slots).filter((k) => k.startsWith(METADATA_PREFIX)); // we need to wait for next tick to ensure that dynamic names slots are picked up await this.$nextTick(); - this.metadataSlots = Object.keys(this.$slots).filter(k => k.startsWith(METADATA_PREFIX)); + this.metadataSlots = Object.keys(this.$slots).filter((k) => k.startsWith(METADATA_PREFIX)); }, }; </script> @@ -44,7 +50,7 @@ export default { <template> <div class="gl-display-flex gl-flex-direction-column"> <div class="gl-display-flex gl-justify-content-space-between gl-py-3"> - <div class="gl-flex-direction-column"> + <div class="gl-flex-direction-column gl-flex-grow-1"> <div class="gl-display-flex"> <gl-avatar v-if="avatar" @@ -68,13 +74,23 @@ export default { </div> <div class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3"> - <div - v-for="(row, metadataIndex) in metadataSlots" - :key="metadataIndex" - class="gl-display-flex gl-align-items-center gl-mr-5" - > - <slot :name="row"></slot> - </div> + <template v-if="!metadataLoading"> + <div + v-for="(row, metadataIndex) in metadataSlots" + :key="metadataIndex" + class="gl-display-flex gl-align-items-center gl-mr-5" + > + <slot :name="row"></slot> + </div> + </template> + <template v-else> + <div class="gl-w-full"> + <gl-skeleton-loader :width="960" :height="16" preserve-aspect-ratio="xMinYMax meet"> + <circle cx="6" cy="8" r="6" /> + <rect x="16" y="4" width="200" height="8" rx="4" /> + </gl-skeleton-loader> + </div> + </template> </div> </div> <div v-if="$slots['right-actions']" class="gl-mt-3"> @@ -89,7 +105,7 @@ export default { data-testid="info-message" > <gl-sprintf :message="message.text"> - <template #docLink="{content}"> + <template #docLink="{ content }"> <gl-link :href="message.link" target="_blank">{{ content }}</gl-link> </template> </gl-sprintf> diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue index fe50a459e52..5d4c192c78f 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue @@ -20,7 +20,7 @@ export default { components: { ToastEditor: () => import(/* webpackChunkName: 'toast_editor' */ '@toast-ui/vue-editor').then( - toast => toast.Editor, + (toast) => toast.Editor, ), AddImageModal, InsertVideoModal, diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js index 108c60c3edb..624b5b09b38 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js @@ -17,12 +17,12 @@ const listItemRenderers = [renderListItem]; const softbreakRenderers = [renderSoftbreak]; const executeRenderer = (renderers, node, context) => { - const availableRenderer = renderers.find(renderer => renderer.canRender(node, context)); + const availableRenderer = renderers.find((renderer) => renderer.canRender(node, context)); return availableRenderer ? availableRenderer.render(node, context) : context.origin(); }; -const buildCustomHTMLRenderer = customRenderers => { +const buildCustomHTMLRenderer = (customRenderers) => { const renderersByType = { ...customRenderers, htmlBlock: union(htmlBlockRenderers, customRenderers?.htmlBlock), @@ -34,7 +34,7 @@ const buildCustomHTMLRenderer = customRenderers => { softbreak: union(softbreakRenderers, customRenderers?.softbreak), }; - return mapValues(renderersByType, renderers => { + return mapValues(renderersByType, (renderers) => { return (node, context) => executeRenderer(renderers, node, context); }); }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js index 9744e25a8e1..273e0a59963 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js @@ -9,7 +9,7 @@ const DEFAULTS = { emphasis: '_', }; -const countIndentSpaces = text => { +const countIndentSpaces = (text) => { const matches = text.match(/^\s+/m); return matches ? matches[0].length : 0; @@ -52,7 +52,7 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) => const firstLevelIndentSpacesCount = countIndentSpaces(baseResult) || 1; const reindentedList = baseResult .split('\n') - .map(line => { + .map((line) => { const itemIndentSpacesCount = countIndentSpaces(line); const nestingLevel = Math.ceil(itemIndentSpacesCount / firstLevelIndentSpacesCount); const indentSpaces = repeat(' ', subListIndentSpaces * nestingLevel); diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js index 463e64b4936..be78651d38d 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js @@ -6,7 +6,7 @@ import buildCustomHTMLRenderer from './build_custom_renderer'; import { TOOLBAR_ITEM_CONFIGS, VIDEO_ATTRIBUTES } from '../constants'; import sanitizeHTML from './sanitize_html'; -const buildWrapper = propsData => { +const buildWrapper = (propsData) => { const instance = new Vue({ render(createElement) { return createElement(ToolbarItem, propsData); @@ -17,7 +17,7 @@ const buildWrapper = propsData => { return instance.$el; }; -const buildVideoIframe = src => { +const buildVideoIframe = (src) => { const wrapper = document.createElement('figure'); const iframe = document.createElement('iframe'); const videoAttributes = { ...VIDEO_ATTRIBUTES, src }; @@ -48,7 +48,7 @@ const buildImg = (alt, originalSrc, file) => { return img; }; -export const generateToolbarItem = config => { +export const generateToolbarItem = (config) => { const { icon, classes, event, command, tooltip, isDivider } = config; if (isDivider) { @@ -92,14 +92,14 @@ export const insertVideo = ({ editor }, url) => { } }; -export const getMarkdown = editorInstance => editorInstance.invoke('getMarkdown'); +export const getMarkdown = (editorInstance) => editorInstance.invoke('getMarkdown'); /** * This function allow us to extend Toast UI HTML to Markdown renderer. It is * a temporary measure because Toast UI does not provide an API * to achieve this goal. */ -export const registerHTMLToMarkdownRenderer = editorApi => { +export const registerHTMLToMarkdownRenderer = (editorApi) => { const { renderer } = editorApi.toMarkOptions; Object.assign(editorApi.toMarkOptions, { @@ -107,10 +107,10 @@ export const registerHTMLToMarkdownRenderer = editorApi => { }); }; -export const getEditorOptions = externalOptions => { +export const getEditorOptions = (externalOptions) => { return defaults({ customHTMLRenderer: buildCustomHTMLRenderer(externalOptions?.customRenderers), - toolbarItems: TOOLBAR_ITEM_CONFIGS.map(toolbarItem => generateToolbarItem(toolbarItem)), - customHTMLSanitizer: html => sanitizeHTML(html), + toolbarItems: TOOLBAR_ITEM_CONFIGS.map((toolbarItem) => generateToolbarItem(toolbarItem)), + customHTMLSanitizer: (html) => sanitizeHTML(html), }); }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js index 1dcecd5fb8c..638e5fd6f60 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js @@ -32,20 +32,20 @@ export const buildUneditableCloseTokens = (token, tagType = TAG_TYPES.block) => // Complete helpers (open plus close) -export const buildTextToken = content => buildToken('text', null, { content }); +export const buildTextToken = (content) => buildToken('text', null, { content }); -export const buildUneditableBlockTokens = token => { +export const buildUneditableBlockTokens = (token) => { return [...buildUneditableOpenTokens(token), buildUneditableCloseToken()]; }; -export const buildUneditableInlineTokens = token => { +export const buildUneditableInlineTokens = (token) => { return [ ...buildUneditableOpenTokens(token, TAG_TYPES.inline), buildUneditableCloseToken(TAG_TYPES.inline), ]; }; -export const buildUneditableHtmlAsTextTokens = node => { +export const buildUneditableHtmlAsTextTokens = (node) => { /* Toast UI internally appends ' data-tomark-pass ' attribute flags so it can target certain nested nodes for internal use during Markdown <=> WYSIWYG conversions. In our case, we want diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js index 18bd17d43d9..30012c1123f 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html_block.js @@ -2,7 +2,7 @@ import { buildUneditableHtmlAsTextTokens } from './build_uneditable_token'; import { ALLOWED_VIDEO_ORIGINS } from '../../constants'; import { getURLOrigin } from '~/lib/utils/url_utility'; -const isVideoFrame = html => { +const isVideoFrame = (html) => { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const { @@ -18,6 +18,6 @@ const canRender = ({ type, literal }) => { return type === 'htmlBlock' && !isVideoFrame(literal); }; -const render = node => buildUneditableHtmlAsTextTokens(node); +const render = (node) => buildUneditableHtmlAsTextTokens(node); export default { canRender, render }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text.js index a9c3dfcd728..d7716543b53 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text.js @@ -17,7 +17,7 @@ Regexp notes: */ const identifierInstanceRegex = /((?:\[.+?\]){1}(?:\[\]|\[.+?\])?(?!:))/g; -const isIdentifierInstance = literal => { +const isIdentifierInstance = (literal) => { // Reset lastIndex as global flag in regexp are stateful (https://stackoverflow.com/a/11477448) identifierInstanceRegex.lastIndex = 0; return identifierInstanceRegex.test(literal); @@ -25,9 +25,9 @@ const isIdentifierInstance = literal => { const canRender = ({ literal }) => isIdentifierInstance(literal); -const tokenize = text => { +const tokenize = (text) => { const matches = text.split(identifierInstanceRegex); - const tokens = matches.map(match => { + const tokens = matches.map((match) => { const token = buildTextToken(match); return isIdentifierInstance(match) ? buildUneditableInlineTokens(token) : token; }); diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js index 3f9c6291d1b..4829f0f2243 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js @@ -1,6 +1,6 @@ const identifierRegex = /(^\[.+\]: .+)/; -const isIdentifier = text => { +const isIdentifier = (text) => { return identifierRegex.test(text); }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_softbreak.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_softbreak.js index 389ade5f27a..c004e839821 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_softbreak.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_softbreak.js @@ -1,4 +1,4 @@ -const canRender = node => ['emph', 'strong'].includes(node.parent?.type); +const canRender = (node) => ['emph', 'strong'].includes(node.parent?.type); const render = () => ({ type: 'text', content: ' ', diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js index 4cba2c70486..eff5dbf59f2 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js @@ -11,9 +11,9 @@ export const renderUneditableBranch = (_, { entering, origin }) => const attributeDefinitionRegexp = /(^{:.+}$)/; -export const isAttributeDefinition = text => attributeDefinitionRegexp.test(text); +export const isAttributeDefinition = (text) => attributeDefinitionRegexp.test(text); -const findAttributeDefinition = node => { +const findAttributeDefinition = (node) => { const literal = node?.next?.firstChild?.literal || node?.firstChild?.firstChild?.next?.next?.literal; // for headings // for list items; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js index eae2e0335c1..cb0f1d51cb1 100644 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/sanitize_html.js @@ -5,7 +5,7 @@ import { getURLOrigin } from '~/lib/utils/url_utility'; const sanitizer = createSanitizer(window); const ADD_TAGS = ['iframe']; -sanitizer.addHook('uponSanitizeElement', node => { +sanitizer.addHook('uponSanitizeElement', (node) => { if (node.tagName !== 'IFRAME') { return; } @@ -17,6 +17,6 @@ sanitizer.addHook('uponSanitizeElement', node => { } }); -const sanitize = content => sanitizer.sanitize(content, { ADD_TAGS }); +const sanitize = (content) => sanitizer.sanitize(content, { ADD_TAGS }); export default sanitize; diff --git a/app/assets/javascripts/vue_shared/components/select2_select.vue b/app/assets/javascripts/vue_shared/components/select2_select.vue index 3dbf0ccdfa9..6574a5ddfde 100644 --- a/app/assets/javascripts/vue_shared/components/select2_select.vue +++ b/app/assets/javascripts/vue_shared/components/select2_select.vue @@ -26,7 +26,7 @@ export default { $(this.$refs.dropdownInput) .val(this.value) .select2(this.options) - .on('change', event => this.$emit('input', event.target.value)); + .on('change', (event) => this.$emit('input', event.target.value)); }) .catch(() => {}); }, diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue index 1ef3d5627ae..22d86ee25d1 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue @@ -143,7 +143,7 @@ export default { > <slot></slot> </dropdown-value> - <div v-if="canEdit" class="selectbox js-selectbox" style="display: none;"> + <div v-if="canEdit" class="selectbox js-selectbox" style="display: none"> <dropdown-hidden-input v-for="label in context.labels" :key="label.id" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue index 434aabc3df9..795f16f4efc 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue @@ -18,7 +18,7 @@ export default { }, created() { const rawLabelsColors = gon.suggested_label_colors; - this.suggestedColors = Object.keys(rawLabelsColors).map(colorCode => ({ + this.suggestedColors = Object.keys(rawLabelsColors).map((colorCode) => ({ colorCode, title: rawLabelsColors[colorCode], })); diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue index 973cc314ee3..122250d1ce7 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue @@ -20,7 +20,7 @@ export default { const labelsString = this.labels.length ? this.labels .slice(0, 5) - .map(label => label.title) + .map((label) => label.title) .join(', ') : s__('LabelSelect|Labels'); diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue index 55e2fb68275..41308e352e3 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue @@ -25,7 +25,7 @@ export default { }, suggestedColors() { const colorsMap = gon.suggested_label_colors; - return Object.keys(colorsMap).map(color => ({ [color]: colorsMap[color] })); + return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] })); }, }, methods: { diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue index 8ce624aa303..683889b8611 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue @@ -182,9 +182,9 @@ export default { !state.showDropdownButton && !state.showDropdownContents ) { - let filterFn = label => label.touched; + let filterFn = (label) => label.touched; if (this.isDropdownVariantEmbedded) { - filterFn = label => label.set; + filterFn = (label) => label.set; } this.handleDropdownClose(state.labels.filter(filterFn)); } @@ -204,13 +204,13 @@ export default { 'js-btn-cancel-create', 'js-sidebar-dropdown-toggle', ].some( - className => + (className) => target?.classList.contains(className) || target?.parentElement?.classList.contains(className), ); const hadExceptionParent = ['.js-btn-back', '.js-labels-list'].some( - className => $(target).parents(className).length, + (className) => $(target).parents(className).length, ); if ( diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js index 5a30e29cad3..d14f96720b7 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js @@ -9,7 +9,7 @@ import { DropdownVariant } from '../constants'; */ export const dropdownButtonText = (state, getters) => { const selectedLabels = getters.isDropdownVariantSidebar - ? state.labels.filter(label => label.set) + ? state.labels.filter((label) => label.set) : state.selectedLabels; if (!selectedLabels.length) { @@ -28,25 +28,25 @@ export const dropdownButtonText = (state, getters) => { * selectedLabels array. * @param {object} state */ -export const selectedLabelsList = state => state.selectedLabels.map(label => label.id); +export const selectedLabelsList = (state) => state.selectedLabels.map((label) => label.id); /** * Returns boolean representing whether dropdown variant * is `sidebar` * @param {object} state */ -export const isDropdownVariantSidebar = state => state.variant === DropdownVariant.Sidebar; +export const isDropdownVariantSidebar = (state) => state.variant === DropdownVariant.Sidebar; /** * Returns boolean representing whether dropdown variant * is `standalone` * @param {object} state */ -export const isDropdownVariantStandalone = state => state.variant === DropdownVariant.Standalone; +export const isDropdownVariantStandalone = (state) => state.variant === DropdownVariant.Standalone; /** * Returns boolean representing whether dropdown variant * is `embedded` * @param {object} state */ -export const isDropdownVariantEmbedded = state => state.variant === DropdownVariant.Embedded; +export const isDropdownVariantEmbedded = (state) => state.variant === DropdownVariant.Embedded; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js index 54f8c78b4e1..6de436ffd13 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js @@ -33,7 +33,7 @@ export default { // Iterate over every label and add a `set` prop // to determine whether it is already a part of // selectedLabels array. - const selectedLabelIds = state.selectedLabels.map(label => label.id); + const selectedLabelIds = state.selectedLabels.map((label) => label.id); state.labelsFetchInProgress = false; state.labels = labels.reduce((allLabels, label) => { allLabels.push({ @@ -61,7 +61,7 @@ export default { // Find the label to update from all the labels // and change `set` prop value to represent their current state. const labelId = labels.pop()?.id; - const candidateLabel = state.labels.find(label => labelId === label.id); + const candidateLabel = state.labels.find((label) => labelId === label.id); if (candidateLabel) { candidateLabel.touched = true; candidateLabel.set = !candidateLabel.set; diff --git a/app/assets/javascripts/vue_shared/components/split_button.vue b/app/assets/javascripts/vue_shared/components/split_button.vue index 11049028ff6..61b317d0d1d 100644 --- a/app/assets/javascripts/vue_shared/components/split_button.vue +++ b/app/assets/javascripts/vue_shared/components/split_button.vue @@ -2,7 +2,7 @@ import { isString } from 'lodash'; import { GlDropdown, GlDropdownDivider, GlDropdownItem } from '@gitlab/ui'; -const isValidItem = item => +const isValidItem = (item) => isString(item.eventName) && isString(item.title) && isString(item.description); export default { diff --git a/app/assets/javascripts/vue_shared/components/tabs/tabs.js b/app/assets/javascripts/vue_shared/components/tabs/tabs.js index 9b9e4bb47bd..233df96a520 100644 --- a/app/assets/javascripts/vue_shared/components/tabs/tabs.js +++ b/app/assets/javascripts/vue_shared/components/tabs/tabs.js @@ -17,8 +17,8 @@ export default { }, methods: { updateTabs() { - this.tabs = this.$children.filter(child => child.isTab); - this.currentIndex = this.tabs.findIndex(tab => tab.localActive); + this.tabs = this.$children.filter((child) => child.isTab); + this.currentIndex = this.tabs.findIndex((tab) => tab.localActive); }, setTab(e, index) { if (this.stopPropagation) { @@ -48,7 +48,7 @@ export default { href: '#', }, on: { - click: e => this.setTab(e, i), + click: (e) => this.setTab(e, i), }, }, tab.$slots.title || tab.title, diff --git a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue index 3fa8efcd145..f1db26ff4fc 100644 --- a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue @@ -36,14 +36,14 @@ export default { }, computed: { timezones() { - return this.timezoneData.map(timezone => ({ + return this.timezoneData.map((timezone) => ({ formattedTimezone: this.formatTimezone(timezone), identifier: timezone.identifier, })); }, filteredResults() { const lowerCasedSearchTerm = this.searchTerm.toLowerCase(); - return this.timezones.filter(timezone => + return this.timezones.filter((timezone) => timezone.formattedTimezone.toLowerCase().includes(lowerCasedSearchTerm), ); }, diff --git a/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue b/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue index b645758d891..01ba2cf5c39 100644 --- a/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue +++ b/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue @@ -62,7 +62,9 @@ export default { return files.every(this.isFileValid); }, isValidDragDataType({ dataTransfer }) { - return Boolean(dataTransfer && dataTransfer.types.some(t => t === VALID_DATA_TRANSFER_TYPE)); + return Boolean( + dataTransfer && dataTransfer.types.some((t) => t === VALID_DATA_TRANSFER_TYPE), + ); }, ondrop({ dataTransfer = {} }) { this.dragCounter = 0; diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue index ea483416c46..efb99eb0d94 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue @@ -104,7 +104,7 @@ export default { :target="() => $refs.userAvatarImage" :placement="tooltipPlacement" boundary="window" - class="js-user-avatar-image-toolip" + class="js-user-avatar-image-tooltip" > <slot> {{ tooltipText }} </slot> </gl-tooltip> diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue index dbb1a075e76..c957876f8ab 100644 --- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue +++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue @@ -72,7 +72,7 @@ export default { }, computed: { actions() { - return [this.webIdeAction, this.editAction, this.gitpodAction].filter(action => action); + return [this.webIdeAction, this.editAction, this.gitpodAction].filter((action) => action); }, editAction() { if (!this.showEditButton) { diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js index 5511145fba2..5262a15136b 100644 --- a/app/assets/javascripts/vue_shared/constants.js +++ b/app/assets/javascripts/vue_shared/constants.js @@ -54,5 +54,6 @@ export const timeRanges = [ }, ]; -export const defaultTimeRange = timeRanges.find(tr => tr.default); -export const getTimeWindow = timeWindowName => timeRanges.find(tr => tr.name === timeWindowName); +export const defaultTimeRange = timeRanges.find((tr) => tr.default); +export const getTimeWindow = (timeWindowName) => + timeRanges.find((tr) => tr.name === timeWindowName); diff --git a/app/assets/javascripts/vue_shared/directives/autofocusonshow.js b/app/assets/javascripts/vue_shared/directives/autofocusonshow.js index 4659ec20ceb..1be4612a7e4 100644 --- a/app/assets/javascripts/vue_shared/directives/autofocusonshow.js +++ b/app/assets/javascripts/vue_shared/directives/autofocusonshow.js @@ -11,8 +11,8 @@ export default { inserted(el) { if ('IntersectionObserver' in window) { // Element visibility is dynamic, so we attach observer - el.visibilityObserver = new IntersectionObserver(entries => { - entries.forEach(entry => { + el.visibilityObserver = new IntersectionObserver((entries) => { + entries.forEach((entry) => { // Combining `intersectionRatio > 0` and // element's `offsetParent` presence will // deteremine if element is truely visible diff --git a/app/assets/javascripts/vue_shared/directives/validation.js b/app/assets/javascripts/vue_shared/directives/validation.js index 09bec78edcc..ece09df272c 100644 --- a/app/assets/javascripts/vue_shared/directives/validation.js +++ b/app/assets/javascripts/vue_shared/directives/validation.js @@ -12,19 +12,19 @@ import { s__ } from '~/locale'; */ const defaultFeedbackMap = { valueMissing: { - isInvalid: el => el.validity?.valueMissing, + isInvalid: (el) => el.validity?.valueMissing, message: s__('Please fill out this field.'), }, urlTypeMismatch: { - isInvalid: el => el.type === 'url' && el.validity?.typeMismatch, + isInvalid: (el) => el.type === 'url' && el.validity?.typeMismatch, message: s__('Please enter a valid URL format, ex: http://www.example.com/home'), }, }; const getFeedbackForElement = (feedbackMap, el) => - Object.values(feedbackMap).find(f => f.isInvalid(el))?.message || el.validationMessage; + Object.values(feedbackMap).find((f) => f.isInvalid(el))?.message || el.validationMessage; -const focusFirstInvalidInput = e => { +const focusFirstInvalidInput = (e) => { const { target: formEl } = e; const invalidInput = formEl.querySelector('input:invalid'); @@ -33,7 +33,7 @@ const focusFirstInvalidInput = e => { } }; -const isEveryFieldValid = form => Object.values(form.fields).every(({ state }) => state === true); +const isEveryFieldValid = (form) => Object.values(form.fields).every(({ state }) => state === true); const createValidator = (context, feedbackMap) => ({ el, reportInvalidInput = false }) => { const { form } = context; @@ -86,7 +86,7 @@ const createValidator = (context, feedbackMap) => ({ el, reportInvalidInput = fa * @param {Object<string, { message: string, isValid: ?function}>} customFeedbackMap * @returns {{ inserted: function, update: function }} validateDirective */ -export default function(customFeedbackMap = {}) { +export default function (customFeedbackMap = {}) { const feedbackMap = merge(defaultFeedbackMap, customFeedbackMap); const elDataMap = new WeakMap(); diff --git a/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js b/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js index 3488a44bd0f..e1734809bce 100644 --- a/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js +++ b/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js @@ -1,4 +1,4 @@ -export default Vue => { +export default (Vue) => { Vue.mixin({ provide: { glFeatures: { ...((window.gon && window.gon.features) || {}) }, diff --git a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js index cdbde55901d..be04ff158e7 100644 --- a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js +++ b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js @@ -48,7 +48,7 @@ export default { this.poll.stop(); const queryString = Object.keys(parameters) - .map(parameter => { + .map((parameter) => { const value = parameters[parameter]; // update internal state for UI this[parameter] = value; diff --git a/app/assets/javascripts/vue_shared/security_reports/components/security_summary.vue b/app/assets/javascripts/vue_shared/security_reports/components/security_summary.vue index babb9fddcf6..e3aa25a294e 100644 --- a/app/assets/javascripts/vue_shared/security_reports/components/security_summary.vue +++ b/app/assets/javascripts/vue_shared/security_reports/components/security_summary.vue @@ -34,13 +34,13 @@ export default { <template> <span> <gl-sprintf :message="message.message"> - <template #total="{content}"> + <template #total="{ content }"> <strong>{{ content }}</strong> </template> </gl-sprintf> <span v-if="shouldShowCountMessage" class="gl-font-sm"> <gl-sprintf :message="message.countMessage"> - <template v-for="slotName in $options.slotNames" #[slotName]="{content}"> + <template v-for="slotName in $options.slotNames" #[slotName]="{ content }"> <span :key="slotName"> <strong v-if="message[slotName] > 0" diff --git a/app/assets/javascripts/vue_shared/security_reports/constants.js b/app/assets/javascripts/vue_shared/security_reports/constants.js index 68241a8c5be..dd591f7bba3 100644 --- a/app/assets/javascripts/vue_shared/security_reports/constants.js +++ b/app/assets/javascripts/vue_shared/security_reports/constants.js @@ -5,6 +5,15 @@ export const FEEDBACK_TYPE_ISSUE = 'issue'; export const FEEDBACK_TYPE_MERGE_REQUEST = 'merge_request'; /** + * Security artifact file types + */ +export const REPORT_FILE_TYPES = { + ARCHIVE: 'ARCHIVE', + TRACE: 'TRACE', + METADATA: 'METADATA', +}; + +/** * Security scan report types, as provided by the backend. */ export const REPORT_TYPE_SAST = 'sast'; diff --git a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue index bdbf9957ad4..a6c7b59aa71 100644 --- a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue +++ b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue @@ -97,7 +97,7 @@ export default { projectPath: this.targetProjectFullPath, iid: String(this.mrIid), reportTypes: this.$options.reportTypes.map( - reportType => reportTypeToSecurityReportTypeEnum[reportType], + (reportType) => reportTypeToSecurityReportTypeEnum[reportType], ), }; }, @@ -151,7 +151,7 @@ export default { created() { if (!this.canShowDownloads) { this.checkAvailableSecurityReports(this.$options.reportTypes) - .then(availableSecurityReports => { + .then((availableSecurityReports) => { this.onCheckingAvailableSecurityReports(Array.from(availableSecurityReports)); }) .catch(this.showError); diff --git a/app/assets/javascripts/vue_shared/security_reports/store/getters.js b/app/assets/javascripts/vue_shared/security_reports/store/getters.js index 1e5a60c32fd..443255b0e6a 100644 --- a/app/assets/javascripts/vue_shared/security_reports/store/getters.js +++ b/app/assets/javascripts/vue_shared/security_reports/store/getters.js @@ -3,7 +3,7 @@ import { countVulnerabilities, groupedTextBuilder } from './utils'; import { LOADING, ERROR, SUCCESS } from '~/reports/constants'; import { TRANSLATION_IS_LOADING } from './messages'; -export const summaryCounts = state => +export const summaryCounts = (state) => countVulnerabilities( state.reportTypes.reduce((acc, reportType) => { acc.push(...state[reportType].newIssues); @@ -50,17 +50,17 @@ export const summaryStatus = (state, getters) => { return SUCCESS; }; -export const areReportsLoading = state => - state.reportTypes.some(reportType => state[reportType].isLoading); +export const areReportsLoading = (state) => + state.reportTypes.some((reportType) => state[reportType].isLoading); -export const areAllReportsLoading = state => - state.reportTypes.every(reportType => state[reportType].isLoading); +export const areAllReportsLoading = (state) => + state.reportTypes.every((reportType) => state[reportType].isLoading); -export const allReportsHaveError = state => - state.reportTypes.every(reportType => state[reportType].hasError); +export const allReportsHaveError = (state) => + state.reportTypes.every((reportType) => state[reportType].hasError); -export const anyReportHasError = state => - state.reportTypes.some(reportType => state[reportType].hasError); +export const anyReportHasError = (state) => + state.reportTypes.some((reportType) => state[reportType].hasError); -export const anyReportHasIssues = state => - state.reportTypes.some(reportType => state[reportType].newIssues.length > 0); +export const anyReportHasIssues = (state) => + state.reportTypes.some((reportType) => state[reportType].newIssues.length > 0); diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js index 22a45341c51..0f26e3c30ef 100644 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js +++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js @@ -15,7 +15,7 @@ export const fetchDiff = ({ state, rootState, dispatch }) => { dispatch('requestDiff'); return fetchDiffData(rootState, state.paths.diffEndpoint, 'sast') - .then(data => { + .then((data) => { dispatch('receiveDiffSuccess', data); }) .catch(() => { diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js index c9da824613d..e3ae5435f5d 100644 --- a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js +++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js @@ -15,7 +15,7 @@ export const fetchDiff = ({ state, rootState, dispatch }) => { dispatch('requestDiff'); return fetchDiffData(rootState, state.paths.diffEndpoint, 'secret_detection') - .then(data => { + .then((data) => { dispatch('receiveDiffSuccess', data); }) .catch(() => { diff --git a/app/assets/javascripts/vue_shared/security_reports/store/utils.js b/app/assets/javascripts/vue_shared/security_reports/store/utils.js index c5e786c92b1..fd6613ae11c 100644 --- a/app/assets/javascripts/vue_shared/security_reports/store/utils.js +++ b/app/assets/javascripts/vue_shared/security_reports/store/utils.js @@ -29,7 +29,7 @@ export const fetchDiffData = (state, endpoint, category) => { */ export const enrichVulnerabilityWithFeedback = (vulnerability, feedback = []) => feedback - .filter(fb => fb.project_fingerprint === vulnerability.project_fingerprint) + .filter((fb) => fb.project_fingerprint === vulnerability.project_fingerprint) .reduce((vuln, fb) => { if (fb.feedback_type === FEEDBACK_TYPE_DISMISSAL) { return { @@ -63,7 +63,7 @@ export const enrichVulnerabilityWithFeedback = (vulnerability, feedback = []) => * @returns {Object} */ export const parseDiff = (diff, enrichData) => { - const enrichVulnerability = vulnerability => ({ + const enrichVulnerability = (vulnerability) => ({ ...enrichVulnerabilityWithFeedback(vulnerability, enrichData), category: vulnerability.report_type, title: vulnerability.message || vulnerability.name, diff --git a/app/assets/javascripts/vue_shared/security_reports/utils.js b/app/assets/javascripts/vue_shared/security_reports/utils.js index 827a87f9aaf..ad819bf7081 100644 --- a/app/assets/javascripts/vue_shared/security_reports/utils.js +++ b/app/assets/javascripts/vue_shared/security_reports/utils.js @@ -1,4 +1,18 @@ -import { securityReportTypeEnumToReportType } from './constants'; +import { capitalize } from 'lodash'; +import { + securityReportTypeEnumToReportType, + REPORT_FILE_TYPES, +} from 'ee_else_ce/vue_shared/security_reports/constants'; + +const addReportTypeIfExists = (acc, reportTypes, reportType, getName, downloadPath) => { + if (reportTypes && reportTypes.includes(reportType)) { + acc.push({ + reportType, + name: getName(reportType), + path: downloadPath, + }); + } +}; export const extractSecurityReportArtifacts = (reportTypes, data) => { const jobs = data.project?.mergeRequest?.headPipeline?.jobs?.nodes ?? []; @@ -7,14 +21,21 @@ export const extractSecurityReportArtifacts = (reportTypes, data) => { const artifacts = job.artifacts?.nodes ?? []; artifacts.forEach(({ downloadPath, fileType }) => { - const reportType = securityReportTypeEnumToReportType[fileType]; - if (reportType && reportTypes.includes(reportType)) { - acc.push({ - name: job.name, - reportType, - path: downloadPath, - }); - } + addReportTypeIfExists( + acc, + reportTypes, + securityReportTypeEnumToReportType[fileType], + () => job.name, + downloadPath, + ); + + addReportTypeIfExists( + acc, + reportTypes, + REPORT_FILE_TYPES[fileType], + (reportType) => `${job.name} ${capitalize(reportType)}`, + downloadPath, + ); }); return acc; diff --git a/app/assets/javascripts/vue_shared/translate.js b/app/assets/javascripts/vue_shared/translate.js index e0baf03acc3..616848639f1 100644 --- a/app/assets/javascripts/vue_shared/translate.js +++ b/app/assets/javascripts/vue_shared/translate.js @@ -1,6 +1,6 @@ import { __, n__, s__, sprintf } from '../locale'; -export default Vue => { +export default (Vue) => { Vue.mixin({ methods: { /** diff --git a/app/assets/javascripts/vuex_shared/bindings.js b/app/assets/javascripts/vuex_shared/bindings.js index cc18b41e2de..741690886b7 100644 --- a/app/assets/javascripts/vuex_shared/bindings.js +++ b/app/assets/javascripts/vuex_shared/bindings.js @@ -11,7 +11,7 @@ */ export const mapComputed = (list, defaultUpdateFn, root) => { const result = {}; - list.forEach(item => { + list.forEach((item) => { const [getter, key, updateFn] = typeof item === 'string' ? [false, item, defaultUpdateFn] diff --git a/app/assets/javascripts/whats_new/components/app.vue b/app/assets/javascripts/whats_new/components/app.vue index 560cabd3bba..0a81f172fe9 100644 --- a/app/assets/javascripts/whats_new/components/app.vue +++ b/app/assets/javascripts/whats_new/components/app.vue @@ -71,7 +71,7 @@ export default { this.setDrawerBodyHeight(height); }, featuresForVersion(version) { - return this.features.filter(feature => { + return this.features.filter((feature) => { return feature.release === parseFloat(version); }); }, @@ -90,11 +90,12 @@ export default { ref="drawer" v-gl-resize-observer="handleResize" class="whats-new-drawer" + :z-index="700" :open="open" @close="closeDrawer" > <template #header> - <h4 class="page-title gl-my-2">{{ __("What's new at GitLab") }}</h4> + <h4 class="page-title gl-my-2">{{ __("What's new") }}</h4> </template> <template v-if="features.length"> <gl-infinite-scroll diff --git a/app/assets/javascripts/whats_new/index.js b/app/assets/javascripts/whats_new/index.js index ed0258c3992..6da141cb19a 100644 --- a/app/assets/javascripts/whats_new/index.js +++ b/app/assets/javascripts/whats_new/index.js @@ -6,7 +6,7 @@ import { getStorageKey, setNotification } from './utils/notification'; let whatsNewApp; -export default el => { +export default (el) => { if (whatsNewApp) { store.dispatch('openDrawer'); } else { diff --git a/app/assets/javascripts/whats_new/utils/get_drawer_body_height.js b/app/assets/javascripts/whats_new/utils/get_drawer_body_height.js index 21fc90c34a4..7d6c2392974 100644 --- a/app/assets/javascripts/whats_new/utils/get_drawer_body_height.js +++ b/app/assets/javascripts/whats_new/utils/get_drawer_body_height.js @@ -1,4 +1,4 @@ -export const getDrawerBodyHeight = drawer => { +export const getDrawerBodyHeight = (drawer) => { const drawerViewableHeight = drawer.clientHeight - drawer.getBoundingClientRect().top; const drawerHeaderHeight = drawer.querySelector('.gl-drawer-header').clientHeight; diff --git a/app/assets/javascripts/whats_new/utils/notification.js b/app/assets/javascripts/whats_new/utils/notification.js index f261a089554..52ca8058d1c 100644 --- a/app/assets/javascripts/whats_new/utils/notification.js +++ b/app/assets/javascripts/whats_new/utils/notification.js @@ -1,6 +1,6 @@ -export const getStorageKey = appEl => appEl.getAttribute('data-storage-key'); +export const getStorageKey = (appEl) => appEl.getAttribute('data-storage-key'); -export const setNotification = appEl => { +export const setNotification = (appEl) => { const storageKey = getStorageKey(appEl); const notificationEl = document.querySelector('.header-help'); let notificationCountEl = notificationEl.querySelector('.js-whats-new-notification-count'); diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index ab0b0b02aa8..06ba2496a99 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -1,4 +1,4 @@ -/* eslint-disable consistent-return, class-methods-use-this */ +/* eslint-disable consistent-return */ // Zen Mode (full screen) textarea // @@ -6,10 +6,10 @@ /*= provides zen_mode:leave */ import $ from 'jquery'; -import 'vendor/jquery.scrollTo'; import Dropzone from 'dropzone'; import Mousetrap from 'mousetrap'; import 'mousetrap/plugins/pause/mousetrap-pause'; +import { scrollToElement } from '~/lib/utils/common_utils'; Dropzone.autoDiscover = false; @@ -39,25 +39,21 @@ export default class ZenMode { constructor() { this.active_backdrop = null; this.active_textarea = null; - $(document).on('click', '.js-zen-enter', e => { + $(document).on('click', '.js-zen-enter', (e) => { e.preventDefault(); return $(e.currentTarget).trigger('zen_mode:enter'); }); - $(document).on('click', '.js-zen-leave', e => { + $(document).on('click', '.js-zen-leave', (e) => { e.preventDefault(); return $(e.currentTarget).trigger('zen_mode:leave'); }); - $(document).on('zen_mode:enter', e => { - this.enter( - $(e.target) - .closest('.md-area') - .find('.zen-backdrop'), - ); + $(document).on('zen_mode:enter', (e) => { + this.enter($(e.target).closest('.md-area').find('.zen-backdrop')); }); $(document).on('zen_mode:leave', () => { this.exit(); }); - $(document).on('keydown', e => { + $(document).on('keydown', (e) => { // Esc if (e.keyCode === 27) { e.preventDefault(); @@ -80,7 +76,7 @@ export default class ZenMode { if (this.active_textarea) { Mousetrap.unpause(); this.active_textarea.closest('.zen-backdrop').removeClass('fullscreen'); - this.scrollTo(this.active_textarea); + scrollToElement(this.active_textarea, { duration: 0, offset: -100 }); this.active_textarea = null; this.active_backdrop = null; @@ -90,10 +86,4 @@ export default class ZenMode { } } } - - scrollTo(zenArea) { - return $.scrollTo(zenArea, 0, { - offset: -150, - }); - } } diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss index f56665553ba..42d15635566 100644 --- a/app/assets/stylesheets/_page_specific_files.scss +++ b/app/assets/stylesheets/_page_specific_files.scss @@ -10,7 +10,6 @@ @import './pages/events'; @import './pages/groups'; @import './pages/help'; -@import './pages/incident_management_list'; @import './pages/issuable'; @import './pages/issues/issue_count_badge'; @import './pages/issues'; @@ -28,6 +27,7 @@ @import './pages/profiles/preferences'; @import './pages/projects'; @import './pages/prometheus'; +@import './pages/registry'; @import './pages/runners'; @import './pages/search'; @import './pages/service_desk'; diff --git a/app/assets/stylesheets/components/avatar.scss b/app/assets/stylesheets/components/avatar.scss index 67213eedca8..3c8abe43070 100644 --- a/app/assets/stylesheets/components/avatar.scss +++ b/app/assets/stylesheets/components/avatar.scss @@ -68,7 +68,7 @@ $avatar-sizes: ( ); $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $identicon-blue, $identicon-teal, - $identicon-orange, $gray-darker; + $identicon-orange, $identicon-gray; %avatar-circle { float: left; @@ -125,8 +125,8 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i .identicon { text-align: center; vertical-align: top; - color: $gray-700; - background-color: $gray-darker; + color: $identicon-text-color; + background-color: $identicon-gray; // Sizes @each $size, $size-config in $avatar-sizes { diff --git a/app/assets/stylesheets/components/deployment_instance.scss b/app/assets/stylesheets/components/deployment_instance.scss new file mode 100644 index 00000000000..a8c3400deca --- /dev/null +++ b/app/assets/stylesheets/components/deployment_instance.scss @@ -0,0 +1,91 @@ +.deployment-instance { + width: $gl-padding; + height: $gl-padding; + margin: 1px; + border: 1px solid; + border-radius: $border-radius-small; + position: relative; + + &-succeeded { + background-color: $green-600; + border-color: $green-800; + + &.link:hover { + background-color: $green-800; + border-color: $green-950; + } + } + + &-running { + background-color: $green-300; + border-color: $green-600; + + &.link:hover { + background-color: $green-500; + border-color: $green-800; + } + } + + &-failed { + background-color: $red-600; + border-color: $red-800; + + &::before { + content: ''; + border: 1px solid $white; + background: $white; + transform: rotate(45deg); + position: absolute; + border-radius: 1px; + top: -2px; + bottom: -2px; + } + + &.link:hover { + background-color: $red-800; + border-color: $red-950; + } + } + + &-pending { + background-color: $gray-200; + border-color: $gray-500; + + &.link:hover { + background-color: $gray-300; + border-color: $gray-900; + } + } + + &-unknown { + background-color: $white; + border-color: $gray-500; + + &.link:hover { + background-color: $white; + border-color: $gray-900; + } + } + + &.deployment-instance-canary { + &::after { + width: 7px; + height: 7px; + border: 1px solid $white; + background-color: $orange-300; + border-radius: 50%; + content: ''; + z-index: 1; + } + } + + &-canary-icon { + background-color: transparent; + border: 0; + + &::after { + width: $gl-padding !important; + height: $gl-padding !important; + } + } +} diff --git a/app/assets/stylesheets/components/whats_new.scss b/app/assets/stylesheets/components/whats_new.scss index 51bf2686be2..3e9060e869b 100644 --- a/app/assets/stylesheets/components/whats_new.scss +++ b/app/assets/stylesheets/components/whats_new.scss @@ -57,7 +57,7 @@ } .whats-new-modal-backdrop { - z-index: 9; + z-index: 699; } .whats-new-notification-count { diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss index a1e757afe56..0ada5fabde9 100644 --- a/app/assets/stylesheets/framework/blank.scss +++ b/app/assets/stylesheets/framework/blank.scss @@ -123,6 +123,10 @@ justify-content: center; text-align: center; } + + .blank-state-icon { + min-width: 215px; + } } $experiment-new-project-indigo-700: #41419f; diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss index 48252762546..745d469e3e8 100644 --- a/app/assets/stylesheets/framework/contextual_sidebar.scss +++ b/app/assets/stylesheets/framework/contextual_sidebar.scss @@ -436,11 +436,7 @@ padding: 6px 16px; margin: 0 0 0 -15px; height: 46px; - - i { - font-size: 20px; - color: $gl-text-color-secondary; - } + color: $gl-text-color; @include media-breakpoint-down(sm) { display: flex; diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss index cf9363b77be..499b9c00116 100644 --- a/app/assets/stylesheets/framework/diffs.scss +++ b/app/assets/stylesheets/framework/diffs.scss @@ -588,6 +588,12 @@ table.code { // Merge request diff grid layout .diff-grid { + .diff-td { + // By default min-width is auto with 1fr which causes some overflow problems + // https://gitlab.com/gitlab-org/gitlab/-/issues/296222 + min-width: 0; + } + .diff-grid-row { display: grid; grid-template-columns: 1fr 1fr; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index e2335c184b0..41fc4d3dd4e 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -567,14 +567,13 @@ margin-bottom: 10px; padding: 0 10px; - .fa, .input-icon, .dropdown-input-clear, .dropdown-input-search { position: absolute; top: $gl-padding-8; right: 20px; - color: $dropdown-input-fa-color; + color: $gray-500; font-size: 12px; pointer-events: none; } @@ -796,6 +795,14 @@ .navbar-gitlab { li.dropdown { position: static; + + &.user-counter { + margin-left: 8px !important; + + > a { + padding: 0 4px !important; + } + } } } @@ -979,6 +986,8 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu { .labels-select-wrapper { &.is-standalone { + min-width: $input-md-width; + .labels-select-dropdown-contents { max-height: 350px; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index a6a01c7b090..730e10114c3 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -172,7 +172,7 @@ } li { - .badge.badge-pill { + .badge.badge-pill:not(.merge-request-badge) { box-shadow: none; font-weight: $gl-font-weight-bold; } @@ -438,7 +438,7 @@ .title-container, .navbar-nav { - .badge.badge-pill { + .badge.badge-pill:not(.merge-request-badge) { position: inherit; font-weight: $gl-font-weight-normal; margin-left: -6px; diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss index 73a2170fc68..28577e2801e 100644 --- a/app/assets/stylesheets/framework/highlight.scss +++ b/app/assets/stylesheets/framework/highlight.scss @@ -45,7 +45,8 @@ a { font-family: $monospace-font; - display: block; + display: flex; + justify-content: flex-end; font-size: $code-font-size !important; white-space: nowrap; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 7ba9236b833..e3d02d01496 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -99,6 +99,10 @@ line-height: $list-text-height; color: $gl-text-color-secondary; + @include media-breakpoint-down(xs) { + padding-top: $gl-padding-6; + } + span { margin-right: 15px; } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index d867cc96dbc..bef33bd2ef0 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -58,6 +58,19 @@ height: $gl-padding; } } + + .copy-email-button { // TODO: replace with utility + @include gl-w-full; + @include gl-h-full; + } + + .copy-email-address { + height: 60px; + + &:hover { + background: $gray-100; + } + } } .right-sidebar-expanded { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 808813599c5..674ba1a307b 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -516,9 +516,12 @@ $line-removed-dark-transparent: rgba(246, 53, 85, 0.2); $line-number-old: #f9d7dc; $line-number-new: #ddfbe6; $line-number-select: #fbf2da; +$line-number-commented: #dae5fb; $line-target-blue: $blue-50; $line-select-yellow: #fcf8e7; $line-select-yellow-dark: #f0e2bd; +$line-commented-blue: #e8effc; +$line-commented-blue-dark: #bccef0; $dark-diff-match-bg: rgba($white, 0.3); $dark-diff-match-color: rgba($white, 0.1); $diff-image-info-color: #808080; @@ -546,7 +549,6 @@ $dropdown-vertical-offset: 4px; $dropdown-empty-row-bg: rgba(#000, 0.04); $dropdown-shadow-color: rgba(#000, 0.1); $dropdown-title-btn-color: #bfbfbf; -$dropdown-input-fa-color: #c7c7c7; $dropdown-input-focus-shadow: rgba($blue-300, 0.4); $dropdown-loading-bg: rgba($white, 0.6); $dropdown-chevron-size: 10px; @@ -629,12 +631,14 @@ $note-icon-gutter-width: 55px; /* * Identicon */ +$identicon-text-color: #525252 !default; $identicon-red: #ffebee !default; $identicon-purple: #f3e5f5 !default; $identicon-indigo: #e8eaf6 !default; $identicon-blue: #e3f2fd !default; $identicon-teal: #e0f2f1 !default; $identicon-orange: #fbe9e7 !default; +$identicon-gray: #eee !default; /* * Calendar diff --git a/app/assets/stylesheets/highlight/common.scss b/app/assets/stylesheets/highlight/common.scss index d9b9f3694c1..6c050f33b07 100644 --- a/app/assets/stylesheets/highlight/common.scss +++ b/app/assets/stylesheets/highlight/common.scss @@ -1,4 +1,5 @@ @import '../framework/variables'; +@import './conflict_colors'; @mixin diff-background($background, $idiff, $border) { background: $background; @@ -35,11 +36,11 @@ transition: border-left 0.1s ease-out; &.coverage { - border-left: 3px solid $coverage; + border-left: 4px solid $coverage; } &.no-coverage { - border-left: 3px solid $no-coverage; + border-left: 2px solid $no-coverage; } } @@ -51,3 +52,44 @@ color: darken($color, 15%); } } + +@mixin conflict-colors($theme) { + .diff-line-num { + &.conflict_marker_our, + &.conflict_our { + background-color: map-get($conflict-colors, #{$theme}-header-head-neutral); + border-color: map-get($conflict-colors, #{$theme}-header-head-neutral); + } + + &.conflict_marker_their, + &.conflict_their { + background-color: map-get($conflict-colors, #{$theme}-header-origin-neutral); + border-color: map-get($conflict-colors, #{$theme}-header-origin-neutral); + } + } + + .line_holder { + .line_content, + .line-coverage { + &.conflict_marker_our { + background-color: map-get($conflict-colors, #{$theme}-header-head-neutral); + border-color: map-get($conflict-colors, #{$theme}-header-head-neutral); + } + + &.conflict_marker_their { + background-color: map-get($conflict-colors, #{$theme}-header-origin-neutral); + border-color: map-get($conflict-colors, #{$theme}-header-origin-neutral); + } + + &.conflict_our { + background-color: map-get($conflict-colors, #{$theme}-line-head-neutral); + border-color: map-get($conflict-colors, #{$theme}-line-head-neutral); + } + + &.conflict_their { + background-color: map-get($conflict-colors, #{$theme}-line-origin-neutral); + border-color: map-get($conflict-colors, #{$theme}-line-origin-neutral); + } + } + } +} diff --git a/app/assets/stylesheets/highlight/conflict_colors.scss b/app/assets/stylesheets/highlight/conflict_colors.scss new file mode 100644 index 00000000000..98ca3775b72 --- /dev/null +++ b/app/assets/stylesheets/highlight/conflict_colors.scss @@ -0,0 +1,119 @@ +// Disabled to use the color map for creating color schemes +// scss-lint:disable ColorVariable +$conflict-colors: ( + white-header-head-neutral : #e1fad7, + white-line-head-neutral : #effdec, + white-button-head-neutral : #9adb84, + + white-header-head-chosen : #baf0a8, + white-line-head-chosen : #e1fad7, + white-button-head-chosen : #52c22d, + + white-header-origin-neutral : #e0f0ff, + white-line-origin-neutral : #f2f9ff, + white-button-origin-neutral : #87c2fa, + + white-header-origin-chosen : #add8ff, + white-line-origin-chosen : #e0f0ff, + white-button-origin-chosen : #268ced, + + white-header-not-chosen : #f0f0f0, + white-line-not-chosen : $gray-light, + + dark-header-head-neutral : rgba(#3f3, 0.2), + dark-line-head-neutral : rgba(#3f3, 0.1), + dark-button-head-neutral : #40874f, + + dark-header-head-chosen : rgba(#3f3, 0.33), + dark-line-head-chosen : rgba(#3f3, 0.2), + dark-button-head-chosen : #258537, + + dark-header-origin-neutral : rgba(#2878c9, 0.4), + dark-line-origin-neutral : rgba(#2878c9, 0.3), + dark-button-origin-neutral : #2a5c8c, + + dark-header-origin-chosen : rgba(#2878c9, 0.6), + dark-line-origin-chosen : rgba(#2878c9, 0.4), + dark-button-origin-chosen : #1d6cbf, + + dark-header-not-chosen : rgba(#fff, 0.25), + dark-line-not-chosen : rgba(#fff, 0.1), + + monokai-header-head-neutral : rgba(#a6e22e, 0.25), + monokai-line-head-neutral : rgba(#a6e22e, 0.1), + monokai-button-head-neutral : #376b20, + + monokai-header-head-chosen : rgba(#a6e22e, 0.4), + monokai-line-head-chosen : rgba(#a6e22e, 0.25), + monokai-button-head-chosen : #39800d, + + monokai-header-origin-neutral : rgba(#60d9f1, 0.35), + monokai-line-origin-neutral : rgba(#60d9f1, 0.15), + monokai-button-origin-neutral : #38848c, + + monokai-header-origin-chosen : rgba(#60d9f1, 0.5), + monokai-line-origin-chosen : rgba(#60d9f1, 0.35), + monokai-button-origin-chosen : #3ea4b2, + + monokai-header-not-chosen : rgba(#76715d, 0.24), + monokai-line-not-chosen : rgba(#76715d, 0.1), + + solarized-light-header-head-neutral : rgba(#859900, 0.37), + solarized-light-line-head-neutral : rgba(#859900, 0.2), + solarized-light-button-head-neutral : #afb262, + + solarized-light-header-head-chosen : rgba(#859900, 0.5), + solarized-light-line-head-chosen : rgba(#859900, 0.37), + solarized-light-button-head-chosen : #94993d, + + solarized-light-header-origin-neutral : rgba(#2878c9, 0.37), + solarized-light-line-origin-neutral : rgba(#2878c9, 0.15), + solarized-light-button-origin-neutral : #60a1bf, + + solarized-light-header-origin-chosen : rgba(#2878c9, 0.6), + solarized-light-line-origin-chosen : rgba(#2878c9, 0.37), + solarized-light-button-origin-chosen : #2482b2, + + solarized-light-header-not-chosen : rgba(#839496, 0.37), + solarized-light-line-not-chosen : rgba(#839496, 0.2), + + solarized-dark-header-head-neutral : rgba(#859900, 0.35), + solarized-dark-line-head-neutral : rgba(#859900, 0.15), + solarized-dark-button-head-neutral : #376b20, + + solarized-dark-header-head-chosen : rgba(#859900, 0.5), + solarized-dark-line-head-chosen : rgba(#859900, 0.35), + solarized-dark-button-head-chosen : #39800d, + + solarized-dark-header-origin-neutral : rgba(#2878c9, 0.35), + solarized-dark-line-origin-neutral : rgba(#2878c9, 0.15), + solarized-dark-button-origin-neutral : #086799, + + solarized-dark-header-origin-chosen : rgba(#2878c9, 0.6), + solarized-dark-line-origin-chosen : rgba(#2878c9, 0.35), + solarized-dark-button-origin-chosen : #0082cc, + + solarized_dark_header_not_chosen : rgba(#839496, 0.25), + solarized_dark_line_not_chosen : rgba(#839496, 0.15), + + none_header_head_neutral : $gray-normal, + none_line_head_neutral : $gray-normal, + none_button_head_neutral : $gray-normal, + + none_header_head_chosen : $gray-darker, + none_line_head_chosen : $gray-darker, + none_button_head_chosen : $gray-darker, + + none_header_origin_neutral : $gray-normal, + none_line_origin_neutral : $gray-normal, + none_button_origin_neutral : $gray-normal, + + none_header_origin_chosen : $gray-darker, + none_line_origin_chosen : $gray-darker, + none_button_origin_chosen : $gray-darker, + + none_header_not_chosen : $gray-light, + none_line_not_chosen : $gray-light + +); +// scss-lint:enable ColorVariable diff --git a/app/assets/stylesheets/highlight/themes/dark.scss b/app/assets/stylesheets/highlight/themes/dark.scss index d51d5b7137d..64387fbce09 100644 --- a/app/assets/stylesheets/highlight/themes/dark.scss +++ b/app/assets/stylesheets/highlight/themes/dark.scss @@ -24,8 +24,8 @@ $dark-pre-hll-bg: #373b41; $dark-hll-bg: #373b41; $dark-over-bg: #9f9ab5; $dark-expanded-bg: #3e3e3e; -$dark-coverage: #b5bd68; -$dark-no-coverage: #de935f; +$dark-coverage: #b3e841; +$dark-no-coverage: #ff4f33; $dark-c: #969896; $dark-err: #c66; $dark-k: #b294bb; @@ -198,6 +198,8 @@ $dark-il: #de935f; } } + @include conflict-colors('dark'); + // highlight line via anchor pre .hll { background-color: $dark-pre-hll-bg !important; diff --git a/app/assets/stylesheets/highlight/themes/monokai.scss b/app/assets/stylesheets/highlight/themes/monokai.scss index e690f9c7c74..119908ffba8 100644 --- a/app/assets/stylesheets/highlight/themes/monokai.scss +++ b/app/assets/stylesheets/highlight/themes/monokai.scss @@ -198,6 +198,8 @@ $monokai-gi: #a6e22e; } } + @include conflict-colors('monokai'); + // highlight line via anchor pre .hll { background-color: $monokai-hll !important; diff --git a/app/assets/stylesheets/highlight/themes/solarized-dark.scss b/app/assets/stylesheets/highlight/themes/solarized-dark.scss index 8c532f53182..f95f5393323 100644 --- a/app/assets/stylesheets/highlight/themes/solarized-dark.scss +++ b/app/assets/stylesheets/highlight/themes/solarized-dark.scss @@ -202,6 +202,8 @@ $solarized-dark-il: #2aa198; } } + @include conflict-colors('solarized-dark'); + // highlight line via anchor pre .hll { background-color: $solarized-dark-hll-bg !important; diff --git a/app/assets/stylesheets/highlight/themes/solarized-light.scss b/app/assets/stylesheets/highlight/themes/solarized-light.scss index 1f9042a9534..dc4bc2f32c2 100644 --- a/app/assets/stylesheets/highlight/themes/solarized-light.scss +++ b/app/assets/stylesheets/highlight/themes/solarized-light.scss @@ -210,6 +210,8 @@ $solarized-light-il: #2aa198; } } + @include conflict-colors('solarized-light'); + // highlight line via anchor pre .hll { background-color: $solarized-light-hll-bg !important; diff --git a/app/assets/stylesheets/highlight/themes/white.scss b/app/assets/stylesheets/highlight/themes/white.scss index 6362dd734f6..ed1d9c924c0 100644 --- a/app/assets/stylesheets/highlight/themes/white.scss +++ b/app/assets/stylesheets/highlight/themes/white.scss @@ -1,3 +1,5 @@ .code.white { @import '../white_base'; + + @include conflict-colors('white'); } diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss index bb5ca94af33..777332881f3 100644 --- a/app/assets/stylesheets/highlight/white_base.scss +++ b/app/assets/stylesheets/highlight/white_base.scss @@ -81,6 +81,17 @@ $white-gc-bg: #eaf2f5; .line-numbers, .diff-line-num { background-color: $gray-light; + + &.conflict_marker, + &.conflict_our { + background-color: map-get($conflict-colors, 'white-header-head-neutral'); + border-color: map-get($conflict-colors, 'white-header-head-neutral'); + } + + &.conflict_their { + background-color: map-get($conflict-colors, 'white-header-origin-neutral'); + border-color: map-get($conflict-colors, 'white-header-origin-neutral'); + } } .diff-line-num, @@ -115,7 +126,7 @@ pre.code, .diff-grid-left:hover, .diff-grid-right:hover { - .diff-line-num:not(.empty-cell) { + .diff-line-num:not(.empty-cell):not(.conflict_marker_their):not(.conflict_marker_our) { @include line-number-hover($white-over-bg); } } @@ -203,7 +214,7 @@ pre.code, } .line-coverage { - @include line-coverage-border-color($green-500, $orange-500); + @include line-coverage-border-color($green-400, $red-400); &.old { background-color: $line-removed; diff --git a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss index 52cc7d3449e..093cba3560f 100644 --- a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss +++ b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss @@ -85,13 +85,12 @@ color: var(--ide-input-border, $gl-text-color-tertiary); } - .dropdown-input .fa, .dropdown-input .dropdown-input-clear { - color: var(--ide-input-border, $dropdown-input-fa-color); + color: var(--ide-input-border, $gray-500); } .ide-nav-form .input-icon { - color: var(--ide-input-border, $dropdown-input-fa-color); + color: var(--ide-input-border, $gray-500); } code { diff --git a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss index cc876c9a635..72e2a45565e 100644 --- a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss +++ b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss @@ -148,18 +148,13 @@ &:hover { box-shadow: inset 0 0 0 0.0625rem $dropdown-toggle-active-border-color; - background-color: $gray-darker; - - svg { - fill: $gl-text-color; - } + background-color: var(--gray-50, $gray-50); } .spinner, svg { width: $ci-action-dropdown-svg-size; height: $ci-action-dropdown-svg-size; - fill: $gl-text-color-secondary; position: relative; top: 1px; vertical-align: initial; diff --git a/app/assets/stylesheets/page_bundles/environments.scss b/app/assets/stylesheets/page_bundles/environments.scss index 871f118ea9d..7d5f501d633 100644 --- a/app/assets/stylesheets/page_bundles/environments.scss +++ b/app/assets/stylesheets/page_bundles/environments.scss @@ -129,3 +129,92 @@ width: 38px; } } + +/** + * Deploy boards +*/ +.deploy-board { + background-color: var(--gray-50, $gray-50); + min-height: 20px; + + > .loading-icon, + > .deploy-board-empty, + > .deploy-board-information { + padding: 10px; + } + + .deploy-board-information { + display: flex; + justify-content: space-between; + + .deploy-board-status { + order: 1; + display: flex; + width: 70px; + flex-wrap: wrap; + justify-content: center; + margin: 20px 0 0 5px; + } + + .deploy-board-instances { + order: 2; + margin-left: 20px; + width: 100%; + } + + .deploy-board-canary-ingress { + order: 7; + } + + .deploy-board-actions { + order: 3; + align-self: center; + min-width: 150px; + margin-left: 10px; + } + + &.deploy-board-error-message { + justify-content: center; + } + + .deploy-board-empty-state-svg { + order: 1; + width: 90px; + margin: auto 0 auto 20px; + } + + .deploy-board-empty-state-text { + order: 2; + flex-wrap: wrap; + margin: auto auto 15px 0; + } + + .deploy-board-empty-state-title { + order: 1; + font-size: 17px; + line-height: 40px; + } + } + + .deploy-board-legend .legend-text { + color: var(--gray-900, $gray-900); + font-size: $gl-font-size-small; + font-weight: $gl-font-weight-bold; + line-height: $gl-line-height-14; + } +} + +.deploy-board-icon { + display: none; + + @include media-breakpoint-up(md) { + float: left; + display: block; + } + + i { + cursor: pointer; + color: var(--gray-200, $gray-200); + padding-right: 10px; + } +} diff --git a/app/assets/stylesheets/page_bundles/error_tracking_index.scss b/app/assets/stylesheets/page_bundles/error_tracking_index.scss index 65bddfb7890..5c49bcc0348 100644 --- a/app/assets/stylesheets/page_bundles/error_tracking_index.scss +++ b/app/assets/stylesheets/page_bundles/error_tracking_index.scss @@ -5,6 +5,10 @@ min-width: auto; } + .filtered-search-box .form-control { + min-width: unset; + } + .sort-control { .btn { padding-right: 2rem; diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 15cc10d1532..f6b9473d235 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -179,6 +179,10 @@ $ide-commit-header-height: 48px; overflow: auto; padding: $gl-padding; background-color: var(--ide-empty-state-background, transparent); + + .md { + max-width: $limited-layout-width; + } } .file-container { diff --git a/app/assets/stylesheets/pages/incident_management_list.scss b/app/assets/stylesheets/page_bundles/incident_management_list.scss index ba363e2d119..30a75103c30 100644 --- a/app/assets/stylesheets/pages/incident_management_list.scss +++ b/app/assets/stylesheets/page_bundles/incident_management_list.scss @@ -1,11 +1,13 @@ +@import 'mixins_and_variables_and_functions'; + .incident-management-list { .new-alert { - background-color: $issues-today-bg; + background-color: var(--green-50, $green-50); } // these styles need to be deleted once GlTable component looks in GitLab same as in @gitlab/ui table { - @include gl-text-gray-500; + color: var(--gray-500, $gray-500); tbody { tr:not(.b-table-busy-slot):not(.b-table-empty-row) { @@ -34,7 +36,8 @@ th { @include gl-bg-transparent; @include gl-font-weight-bold; - @include gl-text-gray-400; + color: var(--gray-400, $gray-400); + &[aria-sort='none']:hover { background-image: url('data:image/svg+xml, %3csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="4 0 8 16"%3e %3cpath style="fill: %23BABABA;" fill-rule="evenodd" d="M11.707085,11.7071 L7.999975,15.4142 L4.292875,11.7071 C3.902375,11.3166 3.902375, 10.6834 4.292875,10.2929 C4.683375,9.90237 5.316575,9.90237 5.707075,10.2929 L6.999975, 11.5858 L6.999975,2 C6.999975,1.44771 7.447695,1 7.999975,1 C8.552255,1 8.999975,1.44771 8.999975,2 L8.999975,11.5858 L10.292865,10.2929 C10.683395 ,9.90237 11.316555,9.90237 11.707085,10.2929 C12.097605,10.6834 12.097605,11.3166 11.707085,11.7071 Z"/%3e %3c/svg%3e'); @@ -67,7 +70,7 @@ } &:hover { - @include gl-bg-white; + background-color: var(--white, $white); @include gl-border-none; } @@ -80,7 +83,7 @@ &.alert-management-table { .table-col { &:last-child { - @include gl-bg-gray-10; + background-color: var(--gray-10, $gray-10); &::before { content: none !important; @@ -120,12 +123,12 @@ @include gl-border-b-0; .gl-tab-nav-item { - @include gl-text-gray-500; + color: var(--gray-500, $gray-500); > .gl-tab-counter-badge { @include gl-reset-color; @include gl-font-sm; - @include gl-bg-gray-50; + background-color: var(--gray-50, $gray-50); } } } diff --git a/app/assets/stylesheets/page_bundles/jira_connect.scss b/app/assets/stylesheets/page_bundles/jira_connect.scss index c3e49da92a6..231723ca4e3 100644 --- a/app/assets/stylesheets/page_bundles/jira_connect.scss +++ b/app/assets/stylesheets/page_bundles/jira_connect.scss @@ -1,6 +1,23 @@ @import 'mixins_and_variables_and_functions'; + +@import '@gitlab/ui/src/scss/bootstrap'; +@import 'bootstrap-vue/src/index'; + +@import '@gitlab/ui/src/scss/utilities'; +@import '@gitlab/ui/src/components/base/alert/alert'; + // We should only import styles that we actually use. -// @import '@gitlab/ui/src/scss/gitlab_ui'; +@import '@gitlab/ui/src/components/base/alert/alert'; +@import '@gitlab/ui/src/components/base/avatar/avatar'; +@import '@gitlab/ui/src/components/base/badge/badge'; +@import '@gitlab/ui/src/components/base/button/button'; +@import '@gitlab/ui/src/components/base/icon/icon'; +@import '@gitlab/ui/src/components/base/link/link'; +@import '@gitlab/ui/src/components/base/loading_icon/loading_icon'; +@import '@gitlab/ui/src/components/base/modal/modal'; +@import '@gitlab/ui/src/components/base/pagination/pagination'; +@import '@gitlab/ui/src/components/base/tabs/tabs/tabs'; +@import '@gitlab/ui/src/components/base/tooltip/tooltip'; $atlaskit-border-color: #dfe1e6; @@ -40,14 +57,16 @@ $header-height: 40px; } .jira-connect-user { - float: right; - position: relative; - top: -30px; + font-size: $gl-font-size; + position: fixed; + top: 10px; + right: 20px; } .jira-connect-app { margin-top: $header-height; max-width: 600px; + min-height: 95vh; padding-top: 48px; padding-left: 16px; padding-right: 16px; @@ -108,5 +127,6 @@ svg { } .browser-limitations-notice { + font-size: $gl-font-size; margin-top: 32px; } diff --git a/app/assets/stylesheets/page_bundles/merge_conflicts.scss b/app/assets/stylesheets/page_bundles/merge_conflicts.scss index a26affb10a9..d7473d2c942 100644 --- a/app/assets/stylesheets/page_bundles/merge_conflicts.scss +++ b/app/assets/stylesheets/page_bundles/merge_conflicts.scss @@ -1,183 +1,65 @@ @import 'mixins_and_variables_and_functions'; -// Disabled to use the color map for creating color schemes -// scss-lint:disable ColorVariable -$colors: ( - white-header-head-neutral : #e1fad7, - white-line-head-neutral : #effdec, - white-button-head-neutral : #9adb84, - - white-header-head-chosen : #baf0a8, - white-line-head-chosen : #e1fad7, - white-button-head-chosen : #52c22d, - - white-header-origin-neutral : #e0f0ff, - white-line-origin-neutral : #f2f9ff, - white-button-origin-neutral : #87c2fa, - - white-header-origin-chosen : #add8ff, - white-line-origin-chosen : #e0f0ff, - white-button-origin-chosen : #268ced, - - white-header-not-chosen : #f0f0f0, - white-line-not-chosen : $gray-light, - - dark-header-head-neutral : rgba(#3f3, 0.2), - dark-line-head-neutral : rgba(#3f3, 0.1), - dark-button-head-neutral : #40874f, - - dark-header-head-chosen : rgba(#3f3, 0.33), - dark-line-head-chosen : rgba(#3f3, 0.2), - dark-button-head-chosen : #258537, - - dark-header-origin-neutral : rgba(#2878c9, 0.4), - dark-line-origin-neutral : rgba(#2878c9, 0.3), - dark-button-origin-neutral : #2a5c8c, - - dark-header-origin-chosen : rgba(#2878c9, 0.6), - dark-line-origin-chosen : rgba(#2878c9, 0.4), - dark-button-origin-chosen : #1d6cbf, - - dark-header-not-chosen : rgba(#fff, 0.25), - dark-line-not-chosen : rgba(#fff, 0.1), - - monokai-header-head-neutral : rgba(#a6e22e, 0.25), - monokai-line-head-neutral : rgba(#a6e22e, 0.1), - monokai-button-head-neutral : #376b20, - - monokai-header-head-chosen : rgba(#a6e22e, 0.4), - monokai-line-head-chosen : rgba(#a6e22e, 0.25), - monokai-button-head-chosen : #39800d, - - monokai-header-origin-neutral : rgba(#60d9f1, 0.35), - monokai-line-origin-neutral : rgba(#60d9f1, 0.15), - monokai-button-origin-neutral : #38848c, - - monokai-header-origin-chosen : rgba(#60d9f1, 0.5), - monokai-line-origin-chosen : rgba(#60d9f1, 0.35), - monokai-button-origin-chosen : #3ea4b2, - - monokai-header-not-chosen : rgba(#76715d, 0.24), - monokai-line-not-chosen : rgba(#76715d, 0.1), - - solarized-light-header-head-neutral : rgba(#859900, 0.37), - solarized-light-line-head-neutral : rgba(#859900, 0.2), - solarized-light-button-head-neutral : #afb262, - - solarized-light-header-head-chosen : rgba(#859900, 0.5), - solarized-light-line-head-chosen : rgba(#859900, 0.37), - solarized-light-button-head-chosen : #94993d, - - solarized-light-header-origin-neutral : rgba(#2878c9, 0.37), - solarized-light-line-origin-neutral : rgba(#2878c9, 0.15), - solarized-light-button-origin-neutral : #60a1bf, - - solarized-light-header-origin-chosen : rgba(#2878c9, 0.6), - solarized-light-line-origin-chosen : rgba(#2878c9, 0.37), - solarized-light-button-origin-chosen : #2482b2, - - solarized-light-header-not-chosen : rgba(#839496, 0.37), - solarized-light-line-not-chosen : rgba(#839496, 0.2), - - solarized-dark-header-head-neutral : rgba(#859900, 0.35), - solarized-dark-line-head-neutral : rgba(#859900, 0.15), - solarized-dark-button-head-neutral : #376b20, - - solarized-dark-header-head-chosen : rgba(#859900, 0.5), - solarized-dark-line-head-chosen : rgba(#859900, 0.35), - solarized-dark-button-head-chosen : #39800d, - - solarized-dark-header-origin-neutral : rgba(#2878c9, 0.35), - solarized-dark-line-origin-neutral : rgba(#2878c9, 0.15), - solarized-dark-button-origin-neutral : #086799, - - solarized-dark-header-origin-chosen : rgba(#2878c9, 0.6), - solarized-dark-line-origin-chosen : rgba(#2878c9, 0.35), - solarized-dark-button-origin-chosen : #0082cc, - - solarized_dark_header_not_chosen : rgba(#839496, 0.25), - solarized_dark_line_not_chosen : rgba(#839496, 0.15), - - none_header_head_neutral : $gray-normal, - none_line_head_neutral : $gray-normal, - none_button_head_neutral : $gray-normal, - - none_header_head_chosen : $gray-darker, - none_line_head_chosen : $gray-darker, - none_button_head_chosen : $gray-darker, - - none_header_origin_neutral : $gray-normal, - none_line_origin_neutral : $gray-normal, - none_button_origin_neutral : $gray-normal, - - none_header_origin_chosen : $gray-darker, - none_line_origin_chosen : $gray-darker, - none_button_origin_chosen : $gray-darker, - - none_header_not_chosen : $gray-light, - none_line_not_chosen : $gray-light - -); -// scss-lint:enable ColorVariable +@import '../highlight/conflict_colors'; @mixin color-scheme($color) { .header.line_content, .diff-line-num { &.origin { - background-color: map-get($colors, #{$color}-header-origin-neutral); - border-color: map-get($colors, #{$color}-header-origin-neutral); + background-color: map-get($conflict-colors, #{$color}-header-origin-neutral); + border-color: map-get($conflict-colors, #{$color}-header-origin-neutral); button { - background-color: map-get($colors, #{$color}-button-origin-neutral); - border-color: darken(map-get($colors, #{$color}-button-origin-neutral), 15); + background-color: map-get($conflict-colors, #{$color}-button-origin-neutral); + border-color: darken(map-get($conflict-colors, #{$color}-button-origin-neutral), 15); } &.selected { - background-color: map-get($colors, #{$color}-header-origin-chosen); - border-color: map-get($colors, #{$color}-header-origin-chosen); + background-color: map-get($conflict-colors, #{$color}-header-origin-chosen); + border-color: map-get($conflict-colors, #{$color}-header-origin-chosen); button { - background-color: map-get($colors, #{$color}-button-origin-chosen); - border-color: darken(map-get($colors, #{$color}-button-origin-chosen), 15); + background-color: map-get($conflict-colors, #{$color}-button-origin-chosen); + border-color: darken(map-get($conflict-colors, #{$color}-button-origin-chosen), 15); } } &.unselected { - background-color: map-get($colors, #{$color}-header-not-chosen); - border-color: map-get($colors, #{$color}-header-not-chosen); + background-color: map-get($conflict-colors, #{$color}-header-not-chosen); + border-color: map-get($conflict-colors, #{$color}-header-not-chosen); button { - background-color: lighten(map-get($colors, #{$color}-button-origin-neutral), 15); - border-color: map-get($colors, #{$color}-button-origin-neutral); + background-color: lighten(map-get($conflict-colors, #{$color}-button-origin-neutral), 15); + border-color: map-get($conflict-colors, #{$color}-button-origin-neutral); } } } &.head { - background-color: map-get($colors, #{$color}-header-head-neutral); - border-color: map-get($colors, #{$color}-header-head-neutral); + background-color: map-get($conflict-colors, #{$color}-header-head-neutral); + border-color: map-get($conflict-colors, #{$color}-header-head-neutral); button { - background-color: map-get($colors, #{$color}-button-head-neutral); - border-color: darken(map-get($colors, #{$color}-button-head-neutral), 15); + background-color: map-get($conflict-colors, #{$color}-button-head-neutral); + border-color: darken(map-get($conflict-colors, #{$color}-button-head-neutral), 15); } &.selected { - background-color: map-get($colors, #{$color}-header-head-chosen); - border-color: map-get($colors, #{$color}-header-head-chosen); + background-color: map-get($conflict-colors, #{$color}-header-head-chosen); + border-color: map-get($conflict-colors, #{$color}-header-head-chosen); button { - background-color: map-get($colors, #{$color}-button-head-chosen); - border-color: darken(map-get($colors, #{$color}-button-head-chosen), 15); + background-color: map-get($conflict-colors, #{$color}-button-head-chosen); + border-color: darken(map-get($conflict-colors, #{$color}-button-head-chosen), 15); } } &.unselected { - background-color: map-get($colors, #{$color}-header-not-chosen); - border-color: map-get($colors, #{$color}-header-not-chosen); + background-color: map-get($conflict-colors, #{$color}-header-not-chosen); + border-color: map-get($conflict-colors, #{$color}-header-not-chosen); button { - background-color: lighten(map-get($colors, #{$color}-button-head-neutral), 15); - border-color: map-get($colors, #{$color}-button-head-neutral); + background-color: lighten(map-get($conflict-colors, #{$color}-button-head-neutral), 15); + border-color: map-get($conflict-colors, #{$color}-button-head-neutral); } } } @@ -185,26 +67,26 @@ $colors: ( .line_content { &.origin { - background-color: map-get($colors, #{$color}-line-origin-neutral); + background-color: map-get($conflict-colors, #{$color}-line-origin-neutral); &.selected { - background-color: map-get($colors, #{$color}-line-origin-chosen); + background-color: map-get($conflict-colors, #{$color}-line-origin-chosen); } &.unselected { - background-color: map-get($colors, #{$color}-line-not-chosen); + background-color: map-get($conflict-colors, #{$color}-line-not-chosen); } } &.head { - background-color: map-get($colors, #{$color}-line-head-neutral); + background-color: map-get($conflict-colors, #{$color}-line-head-neutral); &.selected { - background-color: map-get($colors, #{$color}-line-head-chosen); + background-color: map-get($conflict-colors, #{$color}-line-head-chosen); } &.unselected { - background-color: map-get($colors, #{$color}-line-not-chosen); + background-color: map-get($conflict-colors, #{$color}-line-not-chosen); } } } diff --git a/app/assets/stylesheets/page_bundles/oncall_schedules.scss b/app/assets/stylesheets/page_bundles/oncall_schedules.scss index 3c95ecc9bf0..1b190024457 100644 --- a/app/assets/stylesheets/page_bundles/oncall_schedules.scss +++ b/app/assets/stylesheets/page_bundles/oncall_schedules.scss @@ -29,6 +29,22 @@ } } +.rotations-modal { + .gl-card { + min-width: 75%; + } + + &.gl-modal .modal-md { + max-width: 640px; + } + + // TODO: move to gitlab/ui utilities + // https://gitlab.com/gitlab-org/gitlab/-/issues/297502 + .gl-w-fit-content { + width: fit-content; + } +} + //// Copied from roadmaps.scss - adapted for on-call schedules $header-item-height: 72px; $item-height: 40px; @@ -97,8 +113,6 @@ $column-right-gradient: linear-gradient(to right, $gradient-dark-gray 0%, $gradi } .item-label { - @include gl-py-4; - @include gl-pl-4; border-right: $border-style; border-bottom: $border-style; } diff --git a/app/assets/stylesheets/page_bundles/wiki.scss b/app/assets/stylesheets/page_bundles/wiki.scss index eb34e7f3876..9f0fa137910 100644 --- a/app/assets/stylesheets/page_bundles/wiki.scss +++ b/app/assets/stylesheets/page_bundles/wiki.scss @@ -15,11 +15,6 @@ padding: 11px 0; } - .wiki-page-title { - margin: 0; - font-size: 22px; - } - .wiki-last-edit-by { display: block; color: var(--gray-500, $gray-500); diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index e0e10d63f8e..52bd16d1a79 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -181,6 +181,7 @@ .license-selector, .gitignore-selector, .gitlab-ci-yml-selector, + .gitlab-ci-syntax-yml-selector, .dockerfile-selector, .template-type-selector, .metrics-dashboard-selector { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index efca82def92..b99e619cc98 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -124,7 +124,7 @@ $mr-widget-min-height: 69px; padding: $gl-padding; @include media-breakpoint-up(md) { - padding-left: $gl-padding-8 * 7; + margin-left: $gl-spacing-scale-7; } } } @@ -301,7 +301,8 @@ $mr-widget-min-height: 69px; margin: 0 0 0 10px; } - .bold { + .bold, + .gl-font-weight-bold { font-weight: $gl-font-weight-bold; color: $gray-600; margin-left: 10px; @@ -317,7 +318,8 @@ $mr-widget-min-height: 69px; } .spacing, - .bold { + .bold, + .gl-font-weight-bold { vertical-align: middle; } @@ -394,10 +396,6 @@ $mr-widget-min-height: 69px; } } - .mr-widget-help { - padding: 10px 16px 10px ($gl-padding-8 * 7); - } - .ci-coverage { float: right; } @@ -1016,3 +1014,11 @@ $mr-widget-min-height: 69px; vertical-align: middle; } } + +.mr-ready-to-merge-loader { + max-width: 418px; + + > svg { + vertical-align: middle; + } +} diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 0c24ea9ccc6..254ad96bb57 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -100,8 +100,6 @@ color: $orange-600; background-color: $orange-50; border-radius: $border-radius-default $border-radius-default 0 0; - border: 1px solid $border-gray-normal; - border-bottom: 0; padding: 3px 12px; margin: auto; align-items: center; @@ -454,3 +452,9 @@ table { .markdown-selector { color: $blue-600; } + +.comment-warning-wrapper { + .md-area { + border: 0; + } +} diff --git a/app/assets/stylesheets/pages/registry.scss b/app/assets/stylesheets/pages/registry.scss new file mode 100644 index 00000000000..31c6dbd2970 --- /dev/null +++ b/app/assets/stylesheets/pages/registry.scss @@ -0,0 +1,8 @@ +// Workaround for gl-breadcrumb at the last child of the handwritten breadcrumb +// until this gitlab-ui issue is resolved: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1079 +// +// See app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue when this is changed. +.breadcrumbs-container .gl-breadcrumbs { + padding: 0; + box-shadow: none; +} diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 335e177d169..f31b6d96f03 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -43,7 +43,7 @@ .settings-header { position: relative; - padding: 20px 110px 0 0; + padding: 24px 110px 0 0; h4 { margin-top: 0; diff --git a/app/assets/stylesheets/pages/settings_ci_cd.scss b/app/assets/stylesheets/pages/settings_ci_cd.scss index ebf21f58208..346b3f61caa 100644 --- a/app/assets/stylesheets/pages/settings_ci_cd.scss +++ b/app/assets/stylesheets/pages/settings_ci_cd.scss @@ -17,10 +17,6 @@ } } -.registry-placeholder { - min-height: 60px; -} - .auto-devops-card { margin-bottom: $gl-vert-padding; } diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 8f3574a337b..352050f7b01 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -98,6 +98,7 @@ .tree-table { margin-bottom: 0; + table-layout: fixed; tr { border-bottom: 1px solid $white-normal; diff --git a/app/assets/stylesheets/pages/trials.scss b/app/assets/stylesheets/pages/trials.scss index 3fb9054b2b8..55f323b7df7 100644 --- a/app/assets/stylesheets/pages/trials.scss +++ b/app/assets/stylesheets/pages/trials.scss @@ -3,13 +3,13 @@ * MR link https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22716 */ .gl-select2-html5-required-fix div.select2-container+select.select2 { + @include gl-opacity-0; + @include gl-border-0; + @include gl-bg-none; + @include gl-bg-transparent; display: block !important; width: 1px; height: 1px; z-index: -1; - opacity: 0; margin: -3px auto 0; - background-image: none; - background-color: transparent; - border: 0; } diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss index d875f758ead..31a501f3a36 100644 --- a/app/assets/stylesheets/startup/startup-dark.scss +++ b/app/assets/stylesheets/startup/startup-dark.scss @@ -1242,8 +1242,8 @@ body.gl-dark .navbar-gitlab .search form { line-height: 18px; margin: 4px 0 4px 2px; } -.title-container .badge.badge-pill, -.navbar-nav .badge.badge-pill { +.title-container .badge.badge-pill:not(.merge-request-badge), +.navbar-nav .badge.badge-pill:not(.merge-request-badge) { position: inherit; font-weight: 400; margin-left: -6px; diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss index a98e91b32eb..7f6e537af8f 100644 --- a/app/assets/stylesheets/startup/startup-general.scss +++ b/app/assets/stylesheets/startup/startup-general.scss @@ -1163,8 +1163,8 @@ input { line-height: 18px; margin: 4px 0 4px 2px; } -.title-container .badge.badge-pill, -.navbar-nav .badge.badge-pill { +.title-container .badge.badge-pill:not(.merge-request-badge), +.navbar-nav .badge.badge-pill:not(.merge-request-badge) { position: inherit; font-weight: 400; margin-left: -6px; diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss index 7f2bea9bf26..d8f74a2913e 100644 --- a/app/assets/stylesheets/themes/_dark.scss +++ b/app/assets/stylesheets/themes/_dark.scss @@ -168,6 +168,20 @@ body.gl-dark { --black: #{$black}; --svg-status-bg: #{$white}; + + .gl-button.gl-button, + .gl-button.gl-button.btn-block { + &.btn-default, + &.btn-dashed, + &.btn-info, + &.btn-success, + &.btn-danger, + &.btn-warning { + &-tertiary { + mix-blend-mode: screen; + } + } + } } $border-white-light: $gray-900; diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index bf251993c38..ab330ed69c6 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -143,16 +143,3 @@ flex-direction: column !important; } } - -// These will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1091 -.gl-w-10p { - width: 10%; -} - -.gl-w-20p { - width: 20%; -} - -.gl-w-40p { - width: 40%; -} diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 56ec10fa43a..179e6ef60fb 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -31,7 +31,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController feature_category :source_code_management, [:repository, :clear_repository_check_states] feature_category :continuous_integration, [:ci_cd, :reset_registration_token] - feature_category :collection, [:usage_data] + feature_category :usage_ping, [:usage_data] feature_category :integrations, [:integrations] feature_category :pages, [:lets_encrypt_terms_of_service] diff --git a/app/controllers/admin/dev_ops_report_controller.rb b/app/controllers/admin/dev_ops_report_controller.rb index 59b2200fb59..4ebc643be33 100644 --- a/app/controllers/admin/dev_ops_report_controller.rb +++ b/app/controllers/admin/dev_ops_report_controller.rb @@ -3,6 +3,8 @@ class Admin::DevOpsReportController < Admin::ApplicationController include Analytics::UniqueVisitsHelper + helper_method :show_adoption? + track_unique_visits :show, target_id: 'i_analytics_dev_ops_score' feature_category :devops_reports @@ -12,4 +14,10 @@ class Admin::DevOpsReportController < Admin::ApplicationController @metric = DevOpsReport::Metric.order(:created_at).last&.present end # rubocop: enable CodeReuse/ActiveRecord + + def show_adoption? + false + end end + +Admin::DevOpsReportController.prepend_if_ee('EE::Admin::DevOpsReportController') diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index c4564478462..39718793c1d 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -57,6 +57,10 @@ class Admin::ProjectsController < Admin::ApplicationController namespace = Namespace.find_by(id: params[:new_namespace_id]) ::Projects::TransferService.new(@project, current_user, params.dup).execute(namespace) + if @project.errors[:new_namespace].present? + flash[:alert] = @project.errors[:new_namespace].first + end + @project.reset redirect_to admin_project_path(@project) end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b78029a52cd..3cb7373a970 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -103,14 +103,6 @@ class ApplicationController < ActionController::Base head :forbidden, retry_after: Gitlab::Auth::UniqueIpsLimiter.config.unique_ips_limit_time_window end - rescue_from GRPC::Unavailable, Gitlab::Git::CommandError do |exception| - log_exception(exception) - - headers['Retry-After'] = exception.retry_after if exception.respond_to?(:retry_after) - - render_503 - end - def redirect_back_or_default(default: root_path, options: {}) redirect_back(fallback_location: default, **options) end @@ -246,19 +238,6 @@ class ApplicationController < ActionController::Base head :unprocessable_entity end - def render_503 - respond_to do |format| - format.html do - render( - file: Rails.root.join("public", "503"), - layout: false, - status: :service_unavailable - ) - end - format.any { head :service_unavailable } - end - end - def no_cache_headers DEFAULT_GITLAB_NO_CACHE_HEADERS.each do |k, v| headers[k] = v @@ -286,6 +265,14 @@ class ApplicationController < ActionController::Base end end + def stream_csv_headers(csv_filename) + no_cache_headers + stream_headers + + headers['Content-Type'] = 'text/csv; charset=utf-8; header=present' + headers['Content-Disposition'] = "attachment; filename=\"#{csv_filename}\"" + end + def default_cache_control if request.xhr? ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL @@ -468,6 +455,7 @@ class ApplicationController < ActionController::Base project: -> { @project if @project&.persisted? }, namespace: -> { @group if @group&.persisted? }, caller_id: caller_id, + remote_ip: request.ip, feature_category: feature_category) do yield ensure diff --git a/app/controllers/concerns/invisible_captcha_on_signup.rb b/app/controllers/concerns/invisible_captcha_on_signup.rb index 9bea6145ff3..c7fd6d08744 100644 --- a/app/controllers/concerns/invisible_captcha_on_signup.rb +++ b/app/controllers/concerns/invisible_captcha_on_signup.rb @@ -8,7 +8,7 @@ module InvisibleCaptchaOnSignup end def on_honeypot_spam_callback - return unless Feature.enabled?(:invisible_captcha) + return unless Gitlab::CurrentSettings.invisible_captcha_enabled invisible_captcha_honeypot_counter.increment log_request('Invisible_Captcha_Honeypot_Request') @@ -17,7 +17,7 @@ module InvisibleCaptchaOnSignup end def on_timestamp_spam_callback - return unless Feature.enabled?(:invisible_captcha) + return unless Gitlab::CurrentSettings.invisible_captcha_enabled invisible_captcha_timestamp_counter.increment log_request('Invisible_Captcha_Timestamp_Request') diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 3b46a547d47..57d4203ad43 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -217,6 +217,7 @@ module IssuableActions :issuable_ids, :assignee_id, :milestone_id, + :sprint_id, :state_event, :subscription_event, assignee_ids: [], diff --git a/app/controllers/concerns/redis_tracking.rb b/app/controllers/concerns/redis_tracking.rb index d81bd10d5bb..d71935356b8 100644 --- a/app/controllers/concerns/redis_tracking.rb +++ b/app/controllers/concerns/redis_tracking.rb @@ -33,7 +33,7 @@ module RedisTracking return unless metric_feature_enabled?(feature, feature_default_enabled) return unless visitor_id - Gitlab::UsageDataCounters::HLLRedisCounter.track_event(visitor_id, event_name) + Gitlab::UsageDataCounters::HLLRedisCounter.track_event(event_name, values: visitor_id) end def trackable_request? diff --git a/app/controllers/concerns/show_inherited_labels_checker.rb b/app/controllers/concerns/show_inherited_labels_checker.rb deleted file mode 100644 index 9847226f599..00000000000 --- a/app/controllers/concerns/show_inherited_labels_checker.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module ShowInheritedLabelsChecker - extend ActiveSupport::Concern - - private - - def show_inherited_labels?(include_ancestor_groups) - Feature.enabled?(:show_inherited_labels, @project || @group, default_enabled: true) || include_ancestor_groups # rubocop:disable Gitlab/ModuleWithInstanceVariables - end -end diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb index 50c93441dd4..4ec561014a8 100644 --- a/app/controllers/concerns/spammable_actions.rb +++ b/app/controllers/concerns/spammable_actions.rb @@ -32,10 +32,6 @@ module SpammableActions elsif render_recaptcha? ensure_spam_config_loaded! - if params[:recaptcha_verification] - flash[:alert] = _('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.') - end - respond_to do |format| format.html do render :verify @@ -56,9 +52,9 @@ module SpammableActions def spammable_params default_params = { request: request } - recaptcha_check = params[:recaptcha_verification] && + recaptcha_check = recaptcha_response && ensure_spam_config_loaded! && - verify_recaptcha + verify_recaptcha(response: recaptcha_response) return default_params unless recaptcha_check @@ -66,6 +62,23 @@ module SpammableActions spam_log_id: params[:spam_log_id] }.merge(default_params) end + def recaptcha_response + # NOTE: This field name comes from `Recaptcha::ClientHelper#recaptcha_tags` in the recaptcha + # gem, which is called from the HAML `_recaptcha_form.html.haml` form. + # + # It is used in the `Recaptcha::Verify#verify_recaptcha` if the `response` option is not + # passed explicitly. + # + # Instead of relying on this behavior, we are extracting and passing it explicitly. This will + # make it consistent with the newer, modern reCAPTCHA verification process as it will be + # implemented via the GraphQL API and in Vue components via the native reCAPTCHA Javascript API, + # which requires that the recaptcha response param be obtained and passed explicitly. + # + # After this newer GraphQL/JS API process is fully supported by the backend, we can remove this + # (and other) HAML-specific support. + params['g-recaptcha-response'] + end + def spammable raise NotImplementedError, "#{self.class} does not implement #{__method__}" end diff --git a/app/controllers/dashboard/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb index 6fe3d878639..01dc0b1ab00 100644 --- a/app/controllers/dashboard/snippets_controller.rb +++ b/app/controllers/dashboard/snippets_controller.rb @@ -18,6 +18,7 @@ class Dashboard::SnippetsController < Dashboard::ApplicationController .execute .page(params[:page]) .inc_author + .inc_projects_namespace_route return if redirect_out_of_range(@snippets) diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index d210d0f66fd..1fd3cfd11f9 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -9,13 +9,16 @@ class Explore::ProjectsController < Explore::ApplicationController include SortingPreference MIN_SEARCH_LENGTH = 3 + PAGE_LIMIT = 50 before_action :set_non_archived_param before_action :set_sorting - # Limit taken from https://gitlab.com/gitlab-org/gitlab/issues/38357 + # For background information on the limit, see: + # https://gitlab.com/gitlab-org/gitlab/-/issues/38357 + # https://gitlab.com/gitlab-org/gitlab/-/issues/262682 before_action only: [:index, :trending, :starred] do - limit_pages(200) + limit_pages(PAGE_LIMIT) end rescue_from PageOutOfBoundsError, with: :page_out_of_bounds diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index d1b09e1b49e..5df7ff0632a 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -14,10 +14,6 @@ class Groups::GroupMembersController < Groups::ApplicationController # Authorize before_action :authorize_admin_group_member!, except: admin_not_required_endpoints - before_action do - push_frontend_feature_flag(:group_members_filtered_search, @group, default_enabled: true) - end - skip_before_action :check_two_factor_requirement, only: :leave skip_cross_project_access_check :index, :create, :update, :destroy, :request_access, :approve_access_request, :leave, :resend_invite, diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb index 34856f8d84e..c5dd3e1df35 100644 --- a/app/controllers/groups/labels_controller.rb +++ b/app/controllers/groups/labels_controller.rb @@ -2,7 +2,6 @@ class Groups::LabelsController < Groups::ApplicationController include ToggleSubscriptionAction - include ShowInheritedLabelsChecker before_action :label, only: [:edit, :update, :destroy] before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy] @@ -112,7 +111,7 @@ class Groups::LabelsController < Groups::ApplicationController current_user, group_id: @group.id, only_group_labels: options[:only_group_labels], - include_ancestor_groups: show_inherited_labels?(params[:include_ancestor_groups]), + include_ancestor_groups: true, sort: sort, subscribed: options[:subscribed], include_descendant_groups: options[:include_descendant_groups], diff --git a/app/controllers/groups/settings/packages_and_registries_controller.rb b/app/controllers/groups/settings/packages_and_registries_controller.rb new file mode 100644 index 00000000000..dbc1e68742b --- /dev/null +++ b/app/controllers/groups/settings/packages_and_registries_controller.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Groups + module Settings + class PackagesAndRegistriesController < Groups::ApplicationController + before_action :authorize_admin_group! + + feature_category :package_registry + + def index + end + end + end +end diff --git a/app/controllers/import/bulk_imports_controller.rb b/app/controllers/import/bulk_imports_controller.rb index 4417cfe9098..7394e8bf615 100644 --- a/app/controllers/import/bulk_imports_controller.rb +++ b/app/controllers/import/bulk_imports_controller.rb @@ -1,11 +1,15 @@ # frozen_string_literal: true class Import::BulkImportsController < ApplicationController + include ActionView::Helpers::SanitizeHelper + before_action :ensure_group_import_enabled before_action :verify_blocked_uri, only: :status feature_category :importers + POLLING_INTERVAL = 3_000 + rescue_from BulkImports::Clients::Http::ConnectionError, with: :bulk_import_connection_error def configure @@ -32,6 +36,12 @@ class Import::BulkImportsController < ApplicationController render json: :ok end + def realtime_changes + Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL) + + render json: current_user_bulk_imports.to_json(only: [:id], methods: [:status_name]) + end + private def serialized_importable_data @@ -43,7 +53,22 @@ class Import::BulkImportsController < ApplicationController end def importable_data - client.get('groups', top_level_only: true).parsed_response + client.get('groups', query_params).parsed_response + end + + # Default query string params used to fetch groups from GitLab source instance + # + # top_level_only: fetch only top level groups (subgroups are fetched during import itself) + # min_access_level: fetch only groups user has maintainer or above permissions + # search: optional search param to search user's groups by a keyword + def query_params + query_params = { + top_level_only: true, + min_access_level: Gitlab::Access::MAINTAINER + } + + query_params[:search] = sanitized_filter_param if sanitized_filter_param + query_params end def client @@ -131,4 +156,12 @@ class Import::BulkImportsController < ApplicationController access_token: session[access_token_key] } end + + def sanitized_filter_param + @filter ||= sanitize(params[:filter])&.downcase + end + + def current_user_bulk_imports + current_user.bulk_imports.gitlab + end end diff --git a/app/controllers/jira_connect/app_descriptor_controller.rb b/app/controllers/jira_connect/app_descriptor_controller.rb index d1ba8a98c64..137f830e40b 100644 --- a/app/controllers/jira_connect/app_descriptor_controller.rb +++ b/app/controllers/jira_connect/app_descriptor_controller.rb @@ -65,6 +65,8 @@ class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController } modules.merge!(build_information_module) + modules.merge!(deployment_information_module) + modules.merge!(feature_flag_module) modules end @@ -73,17 +75,46 @@ class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController view_context.image_url('gitlab_logo.png') end + # See: https://developer.atlassian.com/cloud/jira/software/modules/deployment/ + def deployment_information_module + { + jiraDeploymentInfoProvider: common_module_properties.merge( + actions: {}, # TODO: list deployments + name: { value: "GitLab Deployments" }, + key: "gitlab-deployments" + ) + } + end + + # see: https://developer.atlassian.com/cloud/jira/software/modules/feature-flag/ + def feature_flag_module + { + jiraFeatureFlagInfoProvider: common_module_properties.merge( + actions: {}, # TODO: create, link and list feature flags https://gitlab.com/gitlab-org/gitlab/-/issues/297386 + name: { + value: 'GitLab Feature Flags' + }, + key: 'gitlab-feature-flags' + ) + } + end + # See: https://developer.atlassian.com/cloud/jira/software/modules/build/ def build_information_module { - jiraBuildInfoProvider: { - homeUrl: HOME_URL, - logoUrl: logo_url, - documentationUrl: DOC_URL, + jiraBuildInfoProvider: common_module_properties.merge( actions: {}, name: { value: "GitLab CI" }, key: "gitlab-ci" - } + ) + } + end + + def common_module_properties + { + homeUrl: HOME_URL, + logoUrl: logo_url, + documentationUrl: DOC_URL } end diff --git a/app/controllers/jira_connect/subscriptions_controller.rb b/app/controllers/jira_connect/subscriptions_controller.rb index 3ff12f29f10..161280a05fc 100644 --- a/app/controllers/jira_connect/subscriptions_controller.rb +++ b/app/controllers/jira_connect/subscriptions_controller.rb @@ -19,6 +19,9 @@ class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController before_action :allow_rendering_in_iframe, only: :index before_action :verify_qsh_claim!, only: :index before_action :authenticate_user!, only: :create + before_action do + push_frontend_feature_flag(:new_jira_connect_ui, type: :development, default_enabled: :yaml) + end def index @subscriptions = current_jira_installation.subscriptions.preload_namespace_route diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb index c2089a0fca3..1ef1e12bb02 100644 --- a/app/controllers/metrics_controller.rb +++ b/app/controllers/metrics_controller.rb @@ -18,9 +18,19 @@ class MetricsController < ActionController::Base render plain: response, content_type: 'text/plain; version=0.0.4' end + def system + render json: system_metrics + end + private def metrics_service @metrics_service ||= MetricsService.new end + + def system_metrics + Gitlab::Metrics::System.summary.merge( + worker_id: Prometheus::PidProvider.worker_id + ) + end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 8f16650a6f2..8c66f45dd79 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -31,6 +31,9 @@ class Projects::BlobController < Projects::ApplicationController before_action :editor_variables, except: [:show, :preview, :diff] before_action :validate_diff_params, only: :diff before_action :set_last_commit_sha, only: [:edit, :update] + before_action only: :new do + record_experiment_user(:ci_syntax_templates, namespace_id: @project.namespace_id) if params[:file_name] == @project.ci_config_path_or_default + end track_redis_hll_event :create, :update, name: 'g_edit_by_sfe', feature: :track_editor_edit_actions, feature_default_enabled: true diff --git a/app/controllers/projects/ci/pipeline_editor_controller.rb b/app/controllers/projects/ci/pipeline_editor_controller.rb index cc391868df0..ef9025ae52f 100644 --- a/app/controllers/projects/ci/pipeline_editor_controller.rb +++ b/app/controllers/projects/ci/pipeline_editor_controller.rb @@ -3,7 +3,7 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController before_action :check_can_collaborate! before_action do - push_frontend_feature_flag(:ci_config_visualization_tab, @project, default_enabled: false) + push_frontend_feature_flag(:ci_config_visualization_tab, @project, default_enabled: :yaml) end feature_category :pipeline_authoring diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb index ab1cf63c885..5c15a5d246c 100644 --- a/app/controllers/projects/cycle_analytics_controller.rb +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -7,7 +7,6 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController include Analytics::UniqueVisitsHelper include GracefulTimeoutHandling - before_action :whitelist_query_limiting, only: [:show] before_action :authorize_read_cycle_analytics! track_unique_visits :show, target_id: 'p_analytics_valuestream' @@ -38,8 +37,4 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController permissions: @cycle_analytics.permissions(user: current_user) } end - - def whitelist_query_limiting - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42671') - end end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index c37abf82fe9..92483607e65 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -15,6 +15,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController push_frontend_feature_flag(:prometheus_computed_alerts) push_frontend_feature_flag(:disable_metric_dashboard_refresh_rate) end + before_action do + push_frontend_feature_flag(:canary_ingress_weight_control, default_enabled: true) + end before_action :authorize_read_environment!, except: [:metrics, :additional_metrics, :metrics_dashboard, :metrics_redirect] before_action :authorize_create_environment!, only: [:new, :create] before_action :authorize_stop_environment!, only: [:stop] diff --git a/app/controllers/projects/feature_flags_controller.rb b/app/controllers/projects/feature_flags_controller.rb index da9dcd1c09c..6b6606c4f41 100644 --- a/app/controllers/projects/feature_flags_controller.rb +++ b/app/controllers/projects/feature_flags_controller.rb @@ -10,12 +10,10 @@ class Projects::FeatureFlagsController < Projects::ApplicationController before_action :feature_flag, only: [:edit, :update, :destroy] - before_action :ensure_legacy_flags_writable!, only: [:update] + before_action :ensure_flag_writable!, only: [:update] before_action do push_frontend_feature_flag(:feature_flag_permissions) - push_frontend_feature_flag(:feature_flags_legacy_read_only, project, default_enabled: true) - push_frontend_feature_flag(:feature_flags_legacy_read_only_override, project) end feature_category :feature_flags @@ -103,10 +101,8 @@ class Projects::FeatureFlagsController < Projects::ApplicationController @feature_flag ||= @noteable = project.operations_feature_flags.find_by_iid!(params[:iid]) end - def ensure_legacy_flags_writable! - if ::Feature.enabled?(:feature_flags_legacy_read_only, project, default_enabled: true) && - ::Feature.disabled?(:feature_flags_legacy_read_only_override, project) && - feature_flag.legacy_flag? + def ensure_flag_writable! + if feature_flag.legacy_flag? render_error_json(['Legacy feature flags are read-only']) end end diff --git a/app/controllers/projects/incident_management/pager_duty_incidents_controller.rb b/app/controllers/projects/incident_management/pager_duty_incidents_controller.rb index f1264ca4a45..408652b4b9e 100644 --- a/app/controllers/projects/incident_management/pager_duty_incidents_controller.rb +++ b/app/controllers/projects/incident_management/pager_duty_incidents_controller.rb @@ -26,7 +26,7 @@ module Projects end def webhook_processor - ::IncidentManagement::PagerDuty::ProcessWebhookService.new(project, nil, payload) + ::IncidentManagement::PagerDuty::ProcessWebhookService.new(project, payload) end def payload diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index ba8e6b90971..3992165d07c 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -2,7 +2,6 @@ class Projects::LabelsController < Projects::ApplicationController include ToggleSubscriptionAction - include ShowInheritedLabelsChecker before_action :check_issuables_available! before_action :label, only: [:edit, :update, :destroy, :promote] @@ -164,7 +163,7 @@ class Projects::LabelsController < Projects::ApplicationController @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id, - include_ancestor_groups: show_inherited_labels?(params[:include_ancestor_groups]), + include_ancestor_groups: true, search: params[:search], subscribed: params[:subscribed], sort: sort).execute diff --git a/app/controllers/projects/mattermosts_controller.rb b/app/controllers/projects/mattermosts_controller.rb index 63a36732d87..ac204427885 100644 --- a/app/controllers/projects/mattermosts_controller.rb +++ b/app/controllers/projects/mattermosts_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Projects::MattermostsController < Projects::ApplicationController - include TriggersHelper + include Ci::TriggersHelper include ActionView::Helpers::AssetUrlHelper layout 'project_settings' diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index 7d3e7759081..858bdc066c1 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -14,7 +14,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap before_action do push_frontend_feature_flag(:merge_request_reviewers, @project, default_enabled: true) push_frontend_feature_flag(:mr_collapsed_approval_rules, @project) - push_frontend_feature_flag(:reviewer_approval_rules, @project) + push_frontend_feature_flag(:reviewer_approval_rules, @project, default_enabled: :yaml) end def new diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb index da19ddf6105..9180b3f6b62 100644 --- a/app/controllers/projects/merge_requests/diffs_controller.rb +++ b/app/controllers/projects/merge_requests/diffs_controller.rb @@ -11,6 +11,8 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic around_action :allow_gitaly_ref_name_caching + after_action :track_viewed_diffs_events, only: [:diffs_batch] + def show render_diffs end @@ -163,7 +165,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic end def render_merge_ref_head_diff? - Gitlab::Utils.to_boolean(params[:diff_head]) && @merge_request.diffable_merge_ref? + Gitlab::Utils.to_boolean(params[:diff_head]) && @merge_request.diffable_merge_ref? && @start_sha.nil? end def note_positions @@ -188,4 +190,16 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic Discussions::CaptureDiffNotePositionsService.new(@merge_request).execute end + + def track_viewed_diffs_events + return if request.headers['DNT'] == '1' + + Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter + .track_mr_diffs_action(merge_request: @merge_request) + + return unless current_user&.view_diffs_file_by_file + + Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter + .track_mr_diffs_single_file_action(merge_request: @merge_request, user: current_user) + end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 382fbfaac25..d452a5e02e2 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -28,22 +28,21 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo before_action :authenticate_user!, only: [:assign_related_issues] before_action :check_user_can_push_to_source_branch!, only: [:rebase] before_action only: [:show] do - push_frontend_feature_flag(:widget_visibility_polling, @project, default_enabled: true) - push_frontend_feature_flag(:mr_commit_neighbor_nav, @project, default_enabled: true) push_frontend_feature_flag(:multiline_comments, @project, default_enabled: true) push_frontend_feature_flag(:file_identifier_hash) push_frontend_feature_flag(:batch_suggestions, @project, default_enabled: true) push_frontend_feature_flag(:approvals_commented_by, @project, default_enabled: true) - push_frontend_feature_flag(:hide_jump_to_next_unresolved_in_threads, default_enabled: true) push_frontend_feature_flag(:merge_request_widget_graphql, @project) - push_frontend_feature_flag(:unified_diff_components, @project) + push_frontend_feature_flag(:drag_comment_selection, @project, default_enabled: true) + push_frontend_feature_flag(:unified_diff_components, @project, default_enabled: true) push_frontend_feature_flag(:default_merge_ref_for_diffs, @project) push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true) push_frontend_feature_flag(:core_security_mr_widget_counts, @project) push_frontend_feature_flag(:core_security_mr_widget_downloads, @project, default_enabled: true) push_frontend_feature_flag(:remove_resolve_note, @project, default_enabled: true) - push_frontend_feature_flag(:test_failure_history, @project) push_frontend_feature_flag(:diffs_gradual_load, @project, default_enabled: true) + push_frontend_feature_flag(:codequality_mr_diff, @project) + push_frontend_feature_flag(:suggestions_custom_commit, @project) record_experiment_user(:invite_members_version_a) record_experiment_user(:invite_members_version_b) @@ -53,7 +52,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo push_frontend_feature_flag(:vue_issuable_sidebar, @project.group) push_frontend_feature_flag(:merge_request_reviewers, @project, default_enabled: true) push_frontend_feature_flag(:mr_collapsed_approval_rules, @project) - push_frontend_feature_flag(:reviewer_approval_rules, @project) + push_frontend_feature_flag(:reviewer_approval_rules, @project, default_enabled: :yaml) end around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions] @@ -69,8 +68,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo :toggle_award_emoji, :toggle_subscription, :update ] - feature_category :code_testing, [:test_reports, :coverage_reports, :terraform_reports] + feature_category :code_testing, [:test_reports, :coverage_reports] feature_category :accessibility_testing, [:accessibility_reports] + feature_category :infrastructure_as_code, [:terraform_reports] def index @merge_requests = @issuables diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb index 5655d3b4c0d..4af7508b935 100644 --- a/app/controllers/projects/pipeline_schedules_controller.rb +++ b/app/controllers/projects/pipeline_schedules_controller.rb @@ -17,7 +17,6 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController @scope = params[:scope] @all_schedules = Ci::PipelineSchedulesFinder.new(@project).execute @schedules = Ci::PipelineSchedulesFinder.new(@project).execute(scope: params[:scope]) - .includes(:last_pipeline) end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/controllers/projects/pipelines/tests_controller.rb b/app/controllers/projects/pipelines/tests_controller.rb index 1c212964df5..924d52898ea 100644 --- a/app/controllers/projects/pipelines/tests_controller.rb +++ b/app/controllers/projects/pipelines/tests_controller.rb @@ -6,6 +6,8 @@ module Projects before_action :authorize_read_build! before_action :builds, only: [:show] + feature_category :code_testing + def summary respond_to do |format| format.json do diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 74513da8675..e44c00e501e 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -17,10 +17,10 @@ class Projects::PipelinesController < Projects::ApplicationController push_frontend_feature_flag(:new_pipeline_form, project, default_enabled: true) push_frontend_feature_flag(:graphql_pipeline_header, project, type: :development, default_enabled: false) push_frontend_feature_flag(:graphql_pipeline_details, project, type: :development, default_enabled: false) - push_frontend_feature_flag(:graphql_pipeline_analytics, project, type: :development) push_frontend_feature_flag(:new_pipeline_form_prefilled_vars, project, type: :development, default_enabled: true) end before_action :ensure_pipeline, only: [:show] + before_action :push_experiment_to_gon, only: :index, if: :html_request? # Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596 before_action :redirect_for_legacy_scope_filter, only: [:index], if: -> { request.format.html? } @@ -45,7 +45,11 @@ class Projects::PipelinesController < Projects::ApplicationController @pipelines_count = limited_pipelines_count(project) respond_to do |format| - format.html + format.html do + record_empty_pipeline_experiment + + render :index + end format.json do Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL) @@ -184,23 +188,6 @@ class Projects::PipelinesController < Projects::ApplicationController end end - def charts - @charts = {} - @counts = {} - - return if Feature.enabled?(:graphql_pipeline_analytics) - - @charts[:week] = Gitlab::Ci::Charts::WeekChart.new(project) - @charts[:month] = Gitlab::Ci::Charts::MonthChart.new(project) - @charts[:year] = Gitlab::Ci::Charts::YearChart.new(project) - @charts[:pipeline_times] = Gitlab::Ci::Charts::PipelineTime.new(project) - - @counts[:total] = @project.all_pipelines.count(:all) - @counts[:success] = @project.all_pipelines.success.count(:all) - @counts[:failed] = @project.all_pipelines.failed.count(:all) - @counts[:total_duration] = @project.all_pipelines.total_duration - end - def test_report respond_to do |format| format.html do @@ -313,6 +300,20 @@ class Projects::PipelinesController < Projects::ApplicationController def index_params params.permit(:scope, :username, :ref, :status) end + + def record_empty_pipeline_experiment + return unless @pipelines_count.to_i == 0 + return if helpers.has_gitlab_ci?(@project) + + record_experiment_user(:pipelines_empty_state) + end + + def push_experiment_to_gon + return unless current_user + + push_frontend_experiment(:pipelines_empty_state, subject: current_user) + frontend_experimentation_tracking_data(:pipelines_empty_state, 'view', project.namespace_id, subject: current_user) + end end Projects::PipelinesController.prepend_if_ee('EE::Projects::PipelinesController') diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 631f627838b..5972b29a298 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -17,17 +17,18 @@ class Projects::ProjectMembersController < Projects::ApplicationController @skip_groups += @project.group.self_and_ancestors_ids if @project.group @group_links = @project.project_group_links - @group_links = @group_links.search(params[:search]) if params[:search].present? + @group_links = @group_links.search(params[:search_groups]) if params[:search_groups].present? - @project_members = MembersFinder + project_members = MembersFinder .new(@project, current_user, params: filter_params) .execute(include_relations: requested_relations) - @project_members = present_members(@project_members.page(params[:page])) + if helpers.can_manage_project_members?(@project) + @invited_members = present_members(project_members.invite) + @requesters = present_members(AccessRequestsFinder.new(@project).execute(current_user)) + end - @requesters = present_members( - AccessRequestsFinder.new(@project).execute(current_user) - ) + @project_members = present_members(project_members.non_invite.page(params[:page])) @project_member = @project.project_members.new end diff --git a/app/controllers/projects/usage_ping_controller.rb b/app/controllers/projects/usage_ping_controller.rb index 9b4ddb326c1..77ee53f2e5d 100644 --- a/app/controllers/projects/usage_ping_controller.rb +++ b/app/controllers/projects/usage_ping_controller.rb @@ -3,7 +3,7 @@ class Projects::UsagePingController < Projects::ApplicationController before_action :authenticate_user! - feature_category :collection + feature_category :usage_ping def web_ide_clientside_preview return render_404 unless Gitlab::CurrentSettings.web_ide_clientside_preview_enabled? diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3744517934a..0c40478d877 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -94,7 +94,7 @@ class ProjectsController < Projects::ApplicationController redirect_to(edit_project_path(@project, anchor: 'js-general-project-settings')) end else - flash.now[:alert] = result[:message] + flash[:alert] = result[:message] @project.reset format.html { render_edit } @@ -197,13 +197,13 @@ class ProjectsController < Projects::ApplicationController end def housekeeping - ::Projects::HousekeepingService.new(@project, :gc).execute + ::Repositories::HousekeepingService.new(@project, :gc).execute redirect_to( project_path(@project), notice: _("Housekeeping successfully started") ) - rescue ::Projects::HousekeepingService::LeaseTaken => ex + rescue ::Repositories::HousekeepingService::LeaseTaken => ex redirect_to( edit_project_path(@project, anchor: 'js-project-advanced-settings'), alert: ex.to_s diff --git a/app/controllers/repositories/lfs_api_controller.rb b/app/controllers/repositories/lfs_api_controller.rb index 9b7cbcb2dfe..2de29da4b45 100644 --- a/app/controllers/repositories/lfs_api_controller.rb +++ b/app/controllers/repositories/lfs_api_controller.rb @@ -103,18 +103,13 @@ module Repositories end def upload_headers - headers = { + { Authorization: authorization_header, # git-lfs v2.5.0 sets the Content-Type based on the uploaded file. This # ensures that Workhorse can intercept the request. - 'Content-Type': LFS_TRANSFER_CONTENT_TYPE + 'Content-Type': LFS_TRANSFER_CONTENT_TYPE, + 'Transfer-Encoding': 'chunked' } - - if Feature.enabled?(:lfs_chunked_encoding, project, default_enabled: true) - headers['Transfer-Encoding'] = 'chunked' - end - - headers end def lfs_check_batch_operation! diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 46245286820..62208d838c1 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -33,10 +33,8 @@ class UsersController < ApplicationController end format.json do - # In 13.8, this endpoint will be removed: - # https://gitlab.com/gitlab-org/gitlab/-/issues/289972 - load_events - pager_json("events/_events", @events.count, events: @events) + msg = "This endpoint is deprecated. Use %s instead." % user_activity_path + render json: { message: msg }, status: :not_found end end end diff --git a/app/enums/.keep b/app/enums/.keep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/app/enums/.keep diff --git a/app/experiments/application_experiment.rb b/app/experiments/application_experiment.rb index e8f7d22bf77..7a8851d11ce 100644 --- a/app/experiments/application_experiment.rb +++ b/app/experiments/application_experiment.rb @@ -19,7 +19,9 @@ class ApplicationExperiment < Gitlab::Experiment private def resolve_variant_name - variant_names.first if Feature.enabled?(name, self, type: :experiment) + return variant_names.first if Feature.enabled?(name, self, type: :experiment) + + nil # Returning nil vs. :control is important for not caching and rollouts. end # Cache is an implementation on top of Gitlab::Redis::SharedState that also @@ -61,7 +63,7 @@ class ApplicationExperiment < Gitlab::Experiment end def hkey(key) - key.split(':') # this assumes the default strategy in gitlab-experiment + key.to_s.split(':') # this assumes the default strategy in gitlab-experiment end def read_entry(key, **options) diff --git a/app/finders/alert_management/alerts_finder.rb b/app/finders/alert_management/alerts_finder.rb index be3b329fb6f..8e0444d324a 100644 --- a/app/finders/alert_management/alerts_finder.rb +++ b/app/finders/alert_management/alerts_finder.rb @@ -32,6 +32,8 @@ module AlertManagement attr_reader :current_user, :project, :params def by_domain(collection) + return collection if params[:iid].present? + collection.with_operations_alerts end diff --git a/app/finders/autocomplete/group_finder.rb b/app/finders/autocomplete/group_finder.rb index dd97ac4c817..90fa48c2dc9 100644 --- a/app/finders/autocomplete/group_finder.rb +++ b/app/finders/autocomplete/group_finder.rb @@ -27,8 +27,7 @@ module Autocomplete # This removes the need for using `return render_404` and similar patterns # in controllers that use this finder. unless Ability.allowed?(current_user, :read_group, group) - raise ActiveRecord::RecordNotFound - .new("Could not find a Group with ID #{group_id}") + raise ActiveRecord::RecordNotFound, "Could not find a Group with ID #{group_id}" end group diff --git a/app/finders/autocomplete/project_finder.rb b/app/finders/autocomplete/project_finder.rb index 3a4696f4c2e..6e51ae4c70d 100644 --- a/app/finders/autocomplete/project_finder.rb +++ b/app/finders/autocomplete/project_finder.rb @@ -25,8 +25,7 @@ module Autocomplete # This removes the need for using `return render_404` and similar patterns # in controllers that use this finder. unless Ability.allowed?(current_user, :read_project, project) - raise ActiveRecord::RecordNotFound - .new("Could not find a Project with ID #{project_id}") + raise ActiveRecord::RecordNotFound, "Could not find a Project with ID #{project_id}" end project diff --git a/app/finders/ci/pipelines_for_merge_request_finder.rb b/app/finders/ci/pipelines_for_merge_request_finder.rb index da8dfc2579a..1f6ee9d75ad 100644 --- a/app/finders/ci/pipelines_for_merge_request_finder.rb +++ b/app/finders/ci/pipelines_for_merge_request_finder.rb @@ -34,11 +34,7 @@ module Ci pipelines = if merge_request.persisted? - if Feature.enabled?(:ci_pipelines_for_merge_request_finder_new_cte, target_project) - pipelines_using_cte - else - pipelines_using_legacy_cte - end + pipelines_using_cte else triggered_for_branch.for_sha(commit_shas) end @@ -49,18 +45,6 @@ module Ci private - def pipelines_using_legacy_cte - cte = Gitlab::SQL::CTE.new(:shas, merge_request.all_commits.select(:sha)) - - source_sha_join = cte.table[:sha].eq(Ci::Pipeline.arel_table[:source_sha]) - merged_result_pipelines = filter_by(triggered_by_merge_request, cte, source_sha_join) - detached_merge_request_pipelines = filter_by_sha(triggered_by_merge_request, cte) - pipelines_for_branch = filter_by_sha(triggered_for_branch, cte) - - Ci::Pipeline.with(cte.to_arel) # rubocop: disable CodeReuse/ActiveRecord - .from_union([merged_result_pipelines, detached_merge_request_pipelines, pipelines_for_branch]) - end - def pipelines_using_cte cte = Gitlab::SQL::CTE.new(:shas, merge_request.all_commits.select(:sha)) diff --git a/app/finders/concerns/finder_methods.rb b/app/finders/concerns/finder_methods.rb index 622cbcf4928..193b52b1694 100644 --- a/app/finders/concerns/finder_methods.rb +++ b/app/finders/concerns/finder_methods.rb @@ -22,7 +22,7 @@ module FinderMethods def raise_not_found_unless_authorized(result) result = if_authorized(result) - raise ActiveRecord::RecordNotFound.new("Couldn't find #{model}") unless result + raise(ActiveRecord::RecordNotFound, "Couldn't find #{model}") unless result result end diff --git a/app/finders/concerns/packages/finder_helper.rb b/app/finders/concerns/packages/finder_helper.rb new file mode 100644 index 00000000000..524e7aa7ff9 --- /dev/null +++ b/app/finders/concerns/packages/finder_helper.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Packages + module FinderHelper + extend ActiveSupport::Concern + + private + + def packages_visible_to_user(user, within_group:) + return ::Packages::Package.none unless within_group + return ::Packages::Package.none unless Ability.allowed?(user, :read_package, within_group) + + projects = projects_visible_to_reporters(user, within_group.self_and_descendants.select(:id)) + ::Packages::Package.for_projects(projects.select(:id)) + end + + def projects_visible_to_user(user, within_group:) + return ::Project.none unless within_group + return ::Project.none unless Ability.allowed?(user, :read_package, within_group) + + projects_visible_to_reporters(user, within_group.self_and_descendants.select(:id)) + end + + def projects_visible_to_reporters(user, namespace_ids) + ::Project.in_namespace(namespace_ids) + .public_or_visible_to_user(user, ::Gitlab::Access::REPORTER) + end + end +end diff --git a/app/finders/concerns/time_frame_filter.rb b/app/finders/concerns/time_frame_filter.rb index d1ebed730f6..7412bea340e 100644 --- a/app/finders/concerns/time_frame_filter.rb +++ b/app/finders/concerns/time_frame_filter.rb @@ -2,7 +2,7 @@ module TimeFrameFilter def by_timeframe(items) - return items unless params[:start_date] && params[:start_date] + return items unless params[:start_date] && params[:end_date] start_date = params[:start_date].to_date end_date = params[:end_date].to_date diff --git a/app/finders/deployments_finder.rb b/app/finders/deployments_finder.rb index 0bb8ce6b4da..4038f93cf2d 100644 --- a/app/finders/deployments_finder.rb +++ b/app/finders/deployments_finder.rb @@ -4,10 +4,10 @@ class DeploymentsFinder attr_reader :project, :params ALLOWED_SORT_VALUES = %w[id iid created_at updated_at ref].freeze - DEFAULT_SORT_VALUE = 'id'.freeze + DEFAULT_SORT_VALUE = 'id' ALLOWED_SORT_DIRECTIONS = %w[asc desc].freeze - DEFAULT_SORT_DIRECTION = 'asc'.freeze + DEFAULT_SORT_DIRECTION = 'asc' def initialize(project, params = {}) @project = project diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 922b53b514d..fc03d5cd90c 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -4,6 +4,9 @@ # # Used to filter Issues and MergeRequests collections by set of params # +# Note: This class is NOT meant to be instantiated. Instead you should +# look at IssuesFinder or EpicsFinder, which inherit from this. +# # Arguments: # klass - actual class like Issue or MergeRequest # current_user - which user use @@ -92,6 +95,10 @@ class IssuableFinder IssuableFinder::Params end + def klass + raise NotImplementedError + end + def initialize(current_user, params = {}) @current_user = current_user @params = params_class.new(params, current_user, klass) @@ -451,6 +458,7 @@ class IssuableFinder def by_release(items) return items unless params.releases? + return items if params.group? # don't allow release filtering at group level if params.filter_by_no_release? items.without_release diff --git a/app/finders/issuable_finder/params.rb b/app/finders/issuable_finder/params.rb index b481afee338..803b30e86ac 100644 --- a/app/finders/issuable_finder/params.rb +++ b/app/finders/issuable_finder/params.rb @@ -108,16 +108,8 @@ class IssuableFinder project_id.present? end - def group - strong_memoize(:group) do - if params[:group_id].is_a?(Group) - params[:group_id] - elsif params[:group_id].present? - Group.find(params[:group_id]) - else - nil - end - end + def group? + group_id.present? end def related_groups @@ -143,10 +135,25 @@ class IssuableFinder end end + def group + strong_memoize(:group) do + next nil unless group? + + group = group_id.is_a?(Group) ? group_id : Group.find(group_id) + group = nil unless Ability.allowed?(current_user, :read_group, group) + + group + end + end + def project_id params[:project_id] end + def group_id + params[:group_id] + end + def projects strong_memoize(:projects) do next [project] if project? @@ -216,14 +223,14 @@ class IssuableFinder strong_memoize(:milestones) do if milestones? if project? - group_id = project.group&.id + project_group_id = project.group&.id project_id = project.id end - group_id = group.id if group + project_group_id = group.id if group search_params = - { title: params[:milestone_title], project_ids: project_id, group_ids: group_id } + { title: params[:milestone_title], project_ids: project_id, group_ids: project_group_id } MilestonesFinder.new(search_params).execute # rubocop: disable CodeReuse/Finder else diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index 978550aedaf..9f9e2afa7fe 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -57,7 +57,7 @@ class MergeRequestsFinder < IssuableFinder end def params_class - MergeRequestsFinder::Params + MergeRequestsFinder.const_get(:Params, false) # rubocop: disable CodeReuse/Finder end def filter_items(_items) @@ -84,7 +84,7 @@ class MergeRequestsFinder < IssuableFinder def by_commit(items) return items unless params[:commit_sha].presence - items.by_commit_sha(params[:commit_sha]) + items.by_related_commit_sha(params[:commit_sha]) end def source_branch diff --git a/app/finders/packages/debian/distributions_finder.rb b/app/finders/packages/debian/distributions_finder.rb new file mode 100644 index 00000000000..e64b6bdfec1 --- /dev/null +++ b/app/finders/packages/debian/distributions_finder.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Packages + module Debian + class DistributionsFinder + def initialize(container, params = {}) + @container, @params = container, params + end + + def execute + collection = relation.with_container(container) + collection = by_codename(collection) + collection = by_suite(collection) + collection = by_codename_or_suite(collection) + collection + end + + private + + attr_reader :container, :params + + def relation + case container + when Project + Packages::Debian::ProjectDistribution + when Group + Packages::Debian::GroupDistribution + else + raise ArgumentError, "Unexpected container type of '#{container.class}'" + end + end + + def by_codename(collection) + return collection unless params[:codename].present? + + collection.with_codename(params[:codename]) + end + + def by_suite(collection) + return collection unless params[:suite].present? + + collection.with_suite(params[:suite]) + end + + def by_codename_or_suite(collection) + return collection unless params[:codename_or_suite].present? + + collection.with_codename_or_suite(params[:codename_or_suite]) + end + end + end +end diff --git a/app/finders/packages/group_packages_finder.rb b/app/finders/packages/group_packages_finder.rb index a51057571f1..860c4068b31 100644 --- a/app/finders/packages/group_packages_finder.rb +++ b/app/finders/packages/group_packages_finder.rb @@ -27,9 +27,9 @@ module Packages .including_tags .for_projects(group_projects_visible_to_current_user.select(:id)) .processed - .has_version .sort_by_attribute("#{params[:order_by]}_#{params[:sort]}") + packages = filter_with_version(packages) packages = filter_by_package_type(packages) packages = filter_by_package_name(packages) packages @@ -72,5 +72,11 @@ module Packages packages.search_by_name(params[:package_name]) end + + def filter_with_version(packages) + return packages if params[:include_versionless].present? + + packages.has_version + end end end diff --git a/app/finders/packages/maven/package_finder.rb b/app/finders/packages/maven/package_finder.rb index 775db12adb7..7e753705cbd 100644 --- a/app/finders/packages/maven/package_finder.rb +++ b/app/finders/packages/maven/package_finder.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Packages module Maven class PackageFinder @@ -27,17 +28,22 @@ module Packages elsif group packages_for_multiple_projects else - packages + ::Packages::Package.none end end def packages_with_path - base.only_maven_packages_with_path(path) + matching_packages = base.only_maven_packages_with_path(path) + matching_packages = matching_packages.order_by_package_file if versionless_package?(matching_packages) + + matching_packages end - # Produces a query that returns all packages. - def packages - ::Packages::Package.all + def versionless_package?(matching_packages) + return if matching_packages.empty? + + # if one matching package is versionless, they all are. + matching_packages.first&.version.nil? end # Produces a query that retrieves packages from a single project. diff --git a/app/finders/packages/nuget/package_finder.rb b/app/finders/packages/nuget/package_finder.rb index e6fb6712d47..8f585f045a1 100644 --- a/app/finders/packages/nuget/package_finder.rb +++ b/app/finders/packages/nuget/package_finder.rb @@ -1,11 +1,15 @@ # frozen_string_literal: true + module Packages module Nuget class PackageFinder + include ::Packages::FinderHelper + MAX_PACKAGES_COUNT = 50 - def initialize(project, package_name:, package_version: nil, limit: MAX_PACKAGES_COUNT) - @project = project + def initialize(current_user, project_or_group, package_name:, package_version: nil, limit: MAX_PACKAGES_COUNT) + @current_user = current_user + @project_or_group = project_or_group @package_name = package_name @package_version = package_version @limit = limit @@ -17,15 +21,32 @@ module Packages private + def base + if project? + @project_or_group.packages + elsif group? + packages_visible_to_user(@current_user, within_group: @project_or_group) + else + ::Packages::Package.none + end + end + def packages - result = @project.packages - .nuget - .has_version - .processed - .with_name_like(@package_name) + result = base.nuget + .has_version + .processed + .with_name_like(@package_name) result = result.with_version(@package_version) if @package_version.present? result end + + def project? + @project_or_group.is_a?(::Project) + end + + def group? + @project_or_group.is_a?(::Group) + end end end end diff --git a/app/finders/packages/packages_finder.rb b/app/finders/packages/packages_finder.rb index 519e8bf9c34..72a63224d2f 100644 --- a/app/finders/packages/packages_finder.rb +++ b/app/finders/packages/packages_finder.rb @@ -18,7 +18,7 @@ module Packages .including_project_route .including_tags .processed - .has_version + packages = filter_with_version(packages) packages = filter_by_package_type(packages) packages = filter_by_package_name(packages) packages = order_packages(packages) @@ -27,6 +27,12 @@ module Packages private + def filter_with_version(packages) + return packages if params[:include_versionless].present? + + packages.has_version + end + def filter_by_package_type(packages) return packages unless params[:package_type] diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 05dc69ebff6..f1df4fbb0d8 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -52,7 +52,7 @@ class ProjectsFinder < UnionFinder collection = Project.wrap_with_cte(collection) if use_cte collection = filter_projects(collection) - if params[:sort] == 'similarity' && params[:search] && Feature.enabled?(:project_finder_similarity_sort) + if params[:sort] == 'similarity' && params[:search] && Feature.enabled?(:project_finder_similarity_sort, current_user) collection.sorted_by_similarity_desc(params[:search]) else sort(collection) diff --git a/app/finders/template_finder.rb b/app/finders/template_finder.rb index a35376e905e..dac7c526474 100644 --- a/app/finders/template_finder.rb +++ b/app/finders/template_finder.rb @@ -7,6 +7,7 @@ class TemplateFinder dockerfiles: ::Gitlab::Template::DockerfileTemplate, gitignores: ::Gitlab::Template::GitignoreTemplate, gitlab_ci_ymls: ::Gitlab::Template::GitlabCiYmlTemplate, + gitlab_ci_syntax_ymls: ::Gitlab::Template::GitlabCiSyntaxYmlTemplate, metrics_dashboard_ymls: ::Gitlab::Template::MetricsDashboardTemplate, issues: ::Gitlab::Template::IssueTemplate, merge_requests: ::Gitlab::Template::MergeRequestTemplate diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb index cc94536bf79..42042406f3f 100644 --- a/app/finders/users_finder.rb +++ b/app/finders/users_finder.rb @@ -34,6 +34,7 @@ class UsersFinder users = User.all.order_id_desc users = by_username(users) users = by_id(users) + users = by_admins(users) users = by_search(users) users = by_blocked(users) users = by_active(users) @@ -62,6 +63,12 @@ class UsersFinder users.id_in(params[:id]) end + def by_admins(users) + return users unless params[:admins] && current_user&.can_read_all_resources? + + users.admins + end + def by_search(users) return users unless params[:search].present? diff --git a/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb b/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb index 17f9b5b5637..32ca6de9b96 100644 --- a/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb +++ b/app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb @@ -18,12 +18,12 @@ module Mutations argument :queue_name, GraphQL::STRING_TYPE, required: true, - description: 'The name of the queue to delete jobs from' + description: 'The name of the queue to delete jobs from.' field :result, Types::Admin::SidekiqQueues::DeleteJobsResponseType, null: true, - description: 'Information about the status of the deletion request' + description: 'Information about the status of the deletion request.' def ready?(**args) unless current_user&.admin? diff --git a/app/graphql/mutations/alert_management/base.rb b/app/graphql/mutations/alert_management/base.rb index 8c6b4005cf8..3a57cb9670d 100644 --- a/app/graphql/mutations/alert_management/base.rb +++ b/app/graphql/mutations/alert_management/base.rb @@ -7,26 +7,26 @@ module Mutations argument :project_path, GraphQL::ID_TYPE, required: true, - description: "The project the alert to mutate is in" + description: "The project the alert to mutate is in." argument :iid, GraphQL::STRING_TYPE, required: true, - description: "The IID of the alert to mutate" + description: "The IID of the alert to mutate." field :alert, Types::AlertManagement::AlertType, null: true, - description: "The alert after mutation" + description: "The alert after mutation." field :todo, Types::TodoType, null: true, - description: "The todo after mutation" + description: "The todo after mutation." field :issue, Types::IssueType, null: true, - description: "The issue created after mutation" + description: "The issue created after mutation." authorize :update_alert_management_alert diff --git a/app/graphql/mutations/alert_management/http_integration/create.rb b/app/graphql/mutations/alert_management/http_integration/create.rb index ddb75e66bb4..ff165d7f302 100644 --- a/app/graphql/mutations/alert_management/http_integration/create.rb +++ b/app/graphql/mutations/alert_management/http_integration/create.rb @@ -10,32 +10,41 @@ module Mutations argument :project_path, GraphQL::ID_TYPE, required: true, - description: 'The project to create the integration in' + description: 'The project to create the integration in.' argument :name, GraphQL::STRING_TYPE, required: true, - description: 'The name of the integration' + description: 'The name of the integration.' argument :active, GraphQL::BOOLEAN_TYPE, required: true, - description: 'Whether the integration is receiving alerts' + description: 'Whether the integration is receiving alerts.' def resolve(args) - project = authorized_find!(full_path: args[:project_path]) + @project = authorized_find!(full_path: args[:project_path]) response ::AlertManagement::HttpIntegrations::CreateService.new( project, current_user, - args.slice(:name, :active) + http_integration_params(args) ).execute end private + attr_reader :project + def find_object(full_path:) resolve_project(full_path: full_path) end + + # overriden in EE + def http_integration_params(args) + args.slice(:name, :active) + end end end end end + +Mutations::AlertManagement::HttpIntegration::Create.prepend_if_ee('::EE::Mutations::AlertManagement::HttpIntegration::Create') diff --git a/app/graphql/mutations/alert_management/http_integration/destroy.rb b/app/graphql/mutations/alert_management/http_integration/destroy.rb index 45d4bd778da..d0420e2bcb5 100644 --- a/app/graphql/mutations/alert_management/http_integration/destroy.rb +++ b/app/graphql/mutations/alert_management/http_integration/destroy.rb @@ -8,7 +8,7 @@ module Mutations argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration], required: true, - description: "The ID of the integration to remove" + description: "The ID of the integration to remove." def resolve(id:) integration = authorized_find!(id: id) diff --git a/app/graphql/mutations/alert_management/http_integration/http_integration_base.rb b/app/graphql/mutations/alert_management/http_integration/http_integration_base.rb index d328eabf244..147df982bec 100644 --- a/app/graphql/mutations/alert_management/http_integration/http_integration_base.rb +++ b/app/graphql/mutations/alert_management/http_integration/http_integration_base.rb @@ -7,7 +7,7 @@ module Mutations field :integration, Types::AlertManagement::HttpIntegrationType, null: true, - description: "The HTTP integration" + description: "The HTTP integration." authorize :admin_operations diff --git a/app/graphql/mutations/alert_management/http_integration/reset_token.rb b/app/graphql/mutations/alert_management/http_integration/reset_token.rb index 3938b38260e..bf73a9eaae7 100644 --- a/app/graphql/mutations/alert_management/http_integration/reset_token.rb +++ b/app/graphql/mutations/alert_management/http_integration/reset_token.rb @@ -8,7 +8,7 @@ module Mutations argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration], required: true, - description: "The ID of the integration to mutate" + description: "The ID of the integration to mutate." def resolve(id:) integration = authorized_find!(id: id) diff --git a/app/graphql/mutations/alert_management/http_integration/update.rb b/app/graphql/mutations/alert_management/http_integration/update.rb index 98e0f7eb14f..431fccaa5e5 100644 --- a/app/graphql/mutations/alert_management/http_integration/update.rb +++ b/app/graphql/mutations/alert_management/http_integration/update.rb @@ -8,15 +8,15 @@ module Mutations argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration], required: true, - description: "The ID of the integration to mutate" + description: "The ID of the integration to mutate." argument :name, GraphQL::STRING_TYPE, required: false, - description: "The name of the integration" + description: "The name of the integration." argument :active, GraphQL::BOOLEAN_TYPE, required: false, - description: "Whether the integration is receiving alerts" + description: "Whether the integration is receiving alerts." def resolve(args) integration = authorized_find!(id: args[:id]) diff --git a/app/graphql/mutations/alert_management/prometheus_integration/create.rb b/app/graphql/mutations/alert_management/prometheus_integration/create.rb index 935ec53795c..c676cde90b4 100644 --- a/app/graphql/mutations/alert_management/prometheus_integration/create.rb +++ b/app/graphql/mutations/alert_management/prometheus_integration/create.rb @@ -10,15 +10,15 @@ module Mutations argument :project_path, GraphQL::ID_TYPE, required: true, - description: 'The project to create the integration in' + description: 'The project to create the integration in.' argument :active, GraphQL::BOOLEAN_TYPE, required: true, - description: 'Whether the integration is receiving alerts' + description: 'Whether the integration is receiving alerts.' argument :api_url, GraphQL::STRING_TYPE, required: true, - description: 'Endpoint at which prometheus can be queried' + description: 'Endpoint at which prometheus can be queried.' def resolve(args) project = authorized_find!(full_path: args[:project_path]) diff --git a/app/graphql/mutations/alert_management/prometheus_integration/prometheus_integration_base.rb b/app/graphql/mutations/alert_management/prometheus_integration/prometheus_integration_base.rb index 6b690ac239a..cb243f49b33 100644 --- a/app/graphql/mutations/alert_management/prometheus_integration/prometheus_integration_base.rb +++ b/app/graphql/mutations/alert_management/prometheus_integration/prometheus_integration_base.rb @@ -7,7 +7,7 @@ module Mutations field :integration, Types::AlertManagement::PrometheusIntegrationType, null: true, - description: "The newly created integration" + description: "The newly created integration." authorize :admin_project diff --git a/app/graphql/mutations/alert_management/prometheus_integration/reset_token.rb b/app/graphql/mutations/alert_management/prometheus_integration/reset_token.rb index effecd8364d..428be091436 100644 --- a/app/graphql/mutations/alert_management/prometheus_integration/reset_token.rb +++ b/app/graphql/mutations/alert_management/prometheus_integration/reset_token.rb @@ -8,7 +8,7 @@ module Mutations argument :id, Types::GlobalIDType[::PrometheusService], required: true, - description: "The ID of the integration to mutate" + description: "The ID of the integration to mutate." def resolve(id:) integration = authorized_find!(id: id) diff --git a/app/graphql/mutations/alert_management/prometheus_integration/update.rb b/app/graphql/mutations/alert_management/prometheus_integration/update.rb index 46f4c23b739..62fb81bca5a 100644 --- a/app/graphql/mutations/alert_management/prometheus_integration/update.rb +++ b/app/graphql/mutations/alert_management/prometheus_integration/update.rb @@ -8,15 +8,15 @@ module Mutations argument :id, Types::GlobalIDType[::PrometheusService], required: true, - description: "The ID of the integration to mutate" + description: "The ID of the integration to mutate." argument :active, GraphQL::BOOLEAN_TYPE, required: false, - description: "Whether the integration is receiving alerts" + description: "Whether the integration is receiving alerts." argument :api_url, GraphQL::STRING_TYPE, required: false, - description: "Endpoint at which prometheus can be queried" + description: "Endpoint at which prometheus can be queried." def resolve(args) integration = authorized_find!(id: args[:id]) diff --git a/app/graphql/mutations/alert_management/update_alert_status.rb b/app/graphql/mutations/alert_management/update_alert_status.rb index 74185dca529..67f8ba9118f 100644 --- a/app/graphql/mutations/alert_management/update_alert_status.rb +++ b/app/graphql/mutations/alert_management/update_alert_status.rb @@ -7,7 +7,7 @@ module Mutations argument :status, Types::AlertManagement::StatusEnum, required: true, - description: 'The status to set the alert' + description: 'The status to set the alert.' def resolve(project_path:, iid:, status:) alert = authorized_find!(project_path: project_path, iid: iid) diff --git a/app/graphql/mutations/award_emojis/base.rb b/app/graphql/mutations/award_emojis/base.rb index 4bd8304c3fc..4f41a9cafd8 100644 --- a/app/graphql/mutations/award_emojis/base.rb +++ b/app/graphql/mutations/award_emojis/base.rb @@ -12,7 +12,7 @@ module Mutations argument :awardable_id, ::Types::GlobalIDType[::Awardable], required: true, - description: 'The global ID of the awardable resource' + description: 'The global ID of the awardable resource.' argument :name, GraphQL::STRING_TYPE, @@ -22,7 +22,7 @@ module Mutations field :award_emoji, Types::AwardEmojis::AwardEmojiType, null: true, - description: 'The award emoji after mutation' + description: 'The award emoji after mutation.' private diff --git a/app/graphql/mutations/boards/destroy.rb b/app/graphql/mutations/boards/destroy.rb index 7c381113d38..8ec13b885d5 100644 --- a/app/graphql/mutations/boards/destroy.rb +++ b/app/graphql/mutations/boards/destroy.rb @@ -8,11 +8,11 @@ module Mutations field :board, Types::BoardType, null: true, - description: 'The board after mutation' + description: 'The board after mutation.' argument :id, ::Types::GlobalIDType[::Board], required: true, - description: 'The global ID of the board to destroy' + description: 'The global ID of the board to destroy.' authorize :admin_board diff --git a/app/graphql/mutations/boards/issues/issue_move_list.rb b/app/graphql/mutations/boards/issues/issue_move_list.rb index 813b6d3cb2a..91dfd9fc3e9 100644 --- a/app/graphql/mutations/boards/issues/issue_move_list.rb +++ b/app/graphql/mutations/boards/issues/issue_move_list.rb @@ -9,31 +9,31 @@ module Mutations argument :board_id, GraphQL::ID_TYPE, required: true, loads: Types::BoardType, - description: 'Global ID of the board that the issue is in' + description: 'Global ID of the board that the issue is in.' argument :project_path, GraphQL::ID_TYPE, required: true, - description: 'Project the issue to mutate is in' + description: 'Project the issue to mutate is in.' argument :iid, GraphQL::STRING_TYPE, required: true, - description: 'IID of the issue to mutate' + description: 'IID of the issue to mutate.' argument :from_list_id, GraphQL::ID_TYPE, required: false, - description: 'ID of the board list that the issue will be moved from' + description: 'ID of the board list that the issue will be moved from.' argument :to_list_id, GraphQL::ID_TYPE, required: false, - description: 'ID of the board list that the issue will be moved to' + description: 'ID of the board list that the issue will be moved to.' argument :move_before_id, GraphQL::ID_TYPE, required: false, - description: 'ID of issue that should be placed before the current issue' + description: 'ID of issue that should be placed before the current issue.' argument :move_after_id, GraphQL::ID_TYPE, required: false, - description: 'ID of issue that should be placed after the current issue' + description: 'ID of issue that should be placed after the current issue.' def ready?(**args) if move_arguments(args).blank? diff --git a/app/graphql/mutations/boards/lists/base.rb b/app/graphql/mutations/boards/lists/base.rb index d244d6bf8dd..34c138bddc9 100644 --- a/app/graphql/mutations/boards/lists/base.rb +++ b/app/graphql/mutations/boards/lists/base.rb @@ -8,12 +8,12 @@ module Mutations argument :board_id, ::Types::GlobalIDType[::Board], required: true, - description: 'Global ID of the issue board to mutate' + description: 'Global ID of the issue board to mutate.' field :list, Types::BoardListType, null: true, - description: 'List of the issue board' + description: 'List of the issue board.' authorize :admin_list diff --git a/app/graphql/mutations/boards/lists/create.rb b/app/graphql/mutations/boards/lists/create.rb index f6df63365b2..9eb9a4d4b87 100644 --- a/app/graphql/mutations/boards/lists/create.rb +++ b/app/graphql/mutations/boards/lists/create.rb @@ -8,11 +8,11 @@ module Mutations argument :backlog, GraphQL::BOOLEAN_TYPE, required: false, - description: 'Create the backlog list' + description: 'Create the backlog list.' argument :label_id, ::Types::GlobalIDType[::Label], required: false, - description: 'Global ID of an existing label' + description: 'Global ID of an existing label.' def ready?(**args) if args.slice(*mutually_exclusive_args).size != 1 diff --git a/app/graphql/mutations/boards/lists/update.rb b/app/graphql/mutations/boards/lists/update.rb index 14502b5174f..d30d1d89bb2 100644 --- a/app/graphql/mutations/boards/lists/update.rb +++ b/app/graphql/mutations/boards/lists/update.rb @@ -13,16 +13,16 @@ module Mutations argument :position, GraphQL::INT_TYPE, required: false, - description: 'Position of list within the board' + description: 'Position of list within the board.' argument :collapsed, GraphQL::BOOLEAN_TYPE, required: false, - description: 'Indicates if list is collapsed for this user' + description: 'Indicates if list is collapsed for this user.' field :list, Types::BoardListType, null: true, - description: 'Mutated list' + description: 'Mutated list.' def resolve(list: nil, **args) raise_resource_not_available_error! unless can_read_list?(list) diff --git a/app/graphql/mutations/boards/update.rb b/app/graphql/mutations/boards/update.rb index 5cb434e41fd..b4f8179829e 100644 --- a/app/graphql/mutations/boards/update.rb +++ b/app/graphql/mutations/boards/update.rb @@ -30,6 +30,8 @@ module Mutations } end + private + def find_object(id:) # TODO: remove this line when the compatibility layer is removed # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 diff --git a/app/graphql/mutations/branches/create.rb b/app/graphql/mutations/branches/create.rb index 214fead2e80..9fe9bef5403 100644 --- a/app/graphql/mutations/branches/create.rb +++ b/app/graphql/mutations/branches/create.rb @@ -9,21 +9,21 @@ module Mutations argument :project_path, GraphQL::ID_TYPE, required: true, - description: 'Project full path the branch is associated with' + description: 'Project full path the branch is associated with.' argument :name, GraphQL::STRING_TYPE, required: true, - description: 'Name of the branch' + description: 'Name of the branch.' argument :ref, GraphQL::STRING_TYPE, required: true, - description: 'Branch name or commit SHA to create branch from' + description: 'Branch name or commit SHA to create branch from.' field :branch, Types::BranchType, null: true, - description: 'Branch after mutation' + description: 'Branch after mutation.' authorize :push_code diff --git a/app/graphql/mutations/ci/base.rb b/app/graphql/mutations/ci/base.rb deleted file mode 100644 index 0ccee5661b7..00000000000 --- a/app/graphql/mutations/ci/base.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module Mutations - module Ci - class Base < BaseMutation - PipelineID = ::Types::GlobalIDType[::Ci::Pipeline] - - argument :id, PipelineID, - required: true, - description: 'The ID of the pipeline to mutate' - - private - - def find_object(id:) - # TODO: remove this line when the compatibility layer is removed - # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 - id = PipelineID.coerce_isolated_input(id) - GlobalID::Locator.locate(id) - end - end - end -end diff --git a/app/graphql/mutations/ci/ci_cd_settings_update.rb b/app/graphql/mutations/ci/ci_cd_settings_update.rb new file mode 100644 index 00000000000..6b7750ee860 --- /dev/null +++ b/app/graphql/mutations/ci/ci_cd_settings_update.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Mutations + module Ci + class CiCdSettingsUpdate < BaseMutation + include FindsProject + + graphql_name 'CiCdSettingsUpdate' + + authorize :admin_project + + argument :full_path, GraphQL::ID_TYPE, + required: true, + description: 'Full Path of the project the settings belong to.' + + argument :keep_latest_artifact, GraphQL::BOOLEAN_TYPE, + required: false, + description: 'Indicates if the latest artifact should be kept for this project.' + + def resolve(full_path:, **args) + project = authorized_find!(full_path) + settings = project.ci_cd_settings + settings.update(args) + + { errors: errors_on_object(settings) } + end + end + end +end diff --git a/app/graphql/mutations/ci/pipeline/base.rb b/app/graphql/mutations/ci/pipeline/base.rb new file mode 100644 index 00000000000..ebfab56e743 --- /dev/null +++ b/app/graphql/mutations/ci/pipeline/base.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Mutations + module Ci + module Pipeline + class Base < BaseMutation + PipelineID = ::Types::GlobalIDType[::Ci::Pipeline] + + argument :id, PipelineID, + required: true, + description: 'The ID of the pipeline to mutate.' + + private + + def find_object(id:) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = PipelineID.coerce_isolated_input(id) + GlobalID::Locator.locate(id) + end + end + end + end +end diff --git a/app/graphql/mutations/ci/pipeline/cancel.rb b/app/graphql/mutations/ci/pipeline/cancel.rb new file mode 100644 index 00000000000..3fb34a37cfc --- /dev/null +++ b/app/graphql/mutations/ci/pipeline/cancel.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Mutations + module Ci + module Pipeline + class Cancel < Base + graphql_name 'PipelineCancel' + + authorize :update_pipeline + + def resolve(id:) + pipeline = authorized_find!(id: id) + + if pipeline.cancelable? + pipeline.cancel_running + { success: true, errors: [] } + else + { success: false, errors: ['Pipeline is not cancelable'] } + end + end + end + end + end +end diff --git a/app/graphql/mutations/ci/pipeline/destroy.rb b/app/graphql/mutations/ci/pipeline/destroy.rb new file mode 100644 index 00000000000..3f933818ce1 --- /dev/null +++ b/app/graphql/mutations/ci/pipeline/destroy.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Mutations + module Ci + module Pipeline + class Destroy < Base + graphql_name 'PipelineDestroy' + + authorize :destroy_pipeline + + def resolve(id:) + pipeline = authorized_find!(id: id) + project = pipeline.project + + result = ::Ci::DestroyPipelineService.new(project, current_user).execute(pipeline) + { + success: result.success?, + errors: result.errors + } + end + end + end + end +end diff --git a/app/graphql/mutations/ci/pipeline/retry.rb b/app/graphql/mutations/ci/pipeline/retry.rb new file mode 100644 index 00000000000..a12330470f0 --- /dev/null +++ b/app/graphql/mutations/ci/pipeline/retry.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Mutations + module Ci + module Pipeline + class Retry < Base + graphql_name 'PipelineRetry' + + field :pipeline, + Types::Ci::PipelineType, + null: true, + description: 'The pipeline after mutation.' + + authorize :update_pipeline + + def resolve(id:) + pipeline = authorized_find!(id: id) + project = pipeline.project + + ::Ci::RetryPipelineService.new(project, current_user).execute(pipeline) + { + pipeline: pipeline, + errors: errors_on_object(pipeline) + } + end + end + end + end +end diff --git a/app/graphql/mutations/ci/pipeline_cancel.rb b/app/graphql/mutations/ci/pipeline_cancel.rb deleted file mode 100644 index bc881e2ac02..00000000000 --- a/app/graphql/mutations/ci/pipeline_cancel.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module Mutations - module Ci - class PipelineCancel < Base - graphql_name 'PipelineCancel' - - authorize :update_pipeline - - def resolve(id:) - pipeline = authorized_find!(id: id) - - if pipeline.cancelable? - pipeline.cancel_running - { success: true, errors: [] } - else - { success: false, errors: ['Pipeline is not cancelable'] } - end - end - end - end -end diff --git a/app/graphql/mutations/ci/pipeline_destroy.rb b/app/graphql/mutations/ci/pipeline_destroy.rb deleted file mode 100644 index bb24d416583..00000000000 --- a/app/graphql/mutations/ci/pipeline_destroy.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module Mutations - module Ci - class PipelineDestroy < Base - graphql_name 'PipelineDestroy' - - authorize :destroy_pipeline - - def resolve(id:) - pipeline = authorized_find!(id: id) - project = pipeline.project - - result = ::Ci::DestroyPipelineService.new(project, current_user).execute(pipeline) - { - success: result.success?, - errors: result.errors - } - end - end - end -end diff --git a/app/graphql/mutations/ci/pipeline_retry.rb b/app/graphql/mutations/ci/pipeline_retry.rb deleted file mode 100644 index 0669bfc449c..00000000000 --- a/app/graphql/mutations/ci/pipeline_retry.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -module Mutations - module Ci - class PipelineRetry < Base - graphql_name 'PipelineRetry' - - field :pipeline, - Types::Ci::PipelineType, - null: true, - description: 'The pipeline after mutation' - - authorize :update_pipeline - - def resolve(id:) - pipeline = authorized_find!(id: id) - project = pipeline.project - - ::Ci::RetryPipelineService.new(project, current_user).execute(pipeline) - { - pipeline: pipeline, - errors: errors_on_object(pipeline) - } - end - end - end -end diff --git a/app/graphql/mutations/commits/create.rb b/app/graphql/mutations/commits/create.rb index 2b9107350fd..ae14401558b 100644 --- a/app/graphql/mutations/commits/create.rb +++ b/app/graphql/mutations/commits/create.rb @@ -9,15 +9,15 @@ module Mutations argument :project_path, GraphQL::ID_TYPE, required: true, - description: 'Project full path the branch is associated with' + description: 'Project full path the branch is associated with.' argument :branch, GraphQL::STRING_TYPE, required: true, - description: 'Name of the branch to commit into, it can be a new branch' + description: 'Name of the branch to commit into, it can be a new branch.' argument :start_branch, GraphQL::STRING_TYPE, required: false, - description: 'If on a new branch, name of the original branch' + description: 'If on a new branch, name of the original branch.' argument :message, GraphQL::STRING_TYPE, @@ -27,12 +27,12 @@ module Mutations argument :actions, [Types::CommitActionType], required: true, - description: 'Array of action hashes to commit as a batch' + description: 'Array of action hashes to commit as a batch.' field :commit, Types::CommitType, null: true, - description: 'The commit after mutation' + description: 'The commit after mutation.' authorize :push_code diff --git a/app/graphql/mutations/concerns/mutations/authorizes_project.rb b/app/graphql/mutations/concerns/mutations/authorizes_project.rb deleted file mode 100644 index 87341525d6c..00000000000 --- a/app/graphql/mutations/concerns/mutations/authorizes_project.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module Mutations - module AuthorizesProject - include ResolvesProject - - def authorized_find_project!(full_path:) - authorized_find!(full_path: full_path) - end - - private - - def find_object(full_path:) - resolve_project(full_path: full_path) - end - end -end diff --git a/app/graphql/mutations/concerns/mutations/finds_project.rb b/app/graphql/mutations/concerns/mutations/finds_project.rb new file mode 100644 index 00000000000..577f9dc90f8 --- /dev/null +++ b/app/graphql/mutations/concerns/mutations/finds_project.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Mutations + module FindsProject + private + + def find_object(full_path) + Project.find_by_full_path(full_path) + end + end +end diff --git a/app/graphql/mutations/concerns/mutations/resolves_namespace.rb b/app/graphql/mutations/concerns/mutations/resolves_namespace.rb new file mode 100644 index 00000000000..35009623c69 --- /dev/null +++ b/app/graphql/mutations/concerns/mutations/resolves_namespace.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Mutations + module ResolvesNamespace + extend ActiveSupport::Concern + + def resolve_namespace(full_path:) + namespace_resolver.resolve(full_path: full_path) + end + + def namespace_resolver + Resolvers::NamespaceResolver.new(object: nil, context: context, field: nil) + end + end +end diff --git a/app/graphql/mutations/concerns/mutations/resolves_resource_parent.rb b/app/graphql/mutations/concerns/mutations/resolves_resource_parent.rb index 04a9abf9529..e2b3f4b046f 100644 --- a/app/graphql/mutations/concerns/mutations/resolves_resource_parent.rb +++ b/app/graphql/mutations/concerns/mutations/resolves_resource_parent.rb @@ -9,11 +9,11 @@ module Mutations included do argument :project_path, GraphQL::ID_TYPE, required: false, - description: 'The project full path the resource is associated with' + description: 'The project full path the resource is associated with.' argument :group_path, GraphQL::ID_TYPE, required: false, - description: 'The group full path the resource is associated with' + description: 'The group full path the resource is associated with.' end def ready?(**args) diff --git a/app/graphql/mutations/concerns/mutations/resolves_subscription.rb b/app/graphql/mutations/concerns/mutations/resolves_subscription.rb index e8c5d0d404d..e26ae7d228c 100644 --- a/app/graphql/mutations/concerns/mutations/resolves_subscription.rb +++ b/app/graphql/mutations/concerns/mutations/resolves_subscription.rb @@ -7,7 +7,7 @@ module Mutations argument :subscribed_state, GraphQL::BOOLEAN_TYPE, required: true, - description: 'The desired state of the subscription' + description: 'The desired state of the subscription.' end def resolve(project_path:, iid:, subscribed_state:) diff --git a/app/graphql/mutations/concerns/mutations/spammable_mutation_fields.rb b/app/graphql/mutations/concerns/mutations/spammable_mutation_fields.rb index 7aef55f8011..e5df8565618 100644 --- a/app/graphql/mutations/concerns/mutations/spammable_mutation_fields.rb +++ b/app/graphql/mutations/concerns/mutations/spammable_mutation_fields.rb @@ -8,7 +8,7 @@ module Mutations field :spam, GraphQL::BOOLEAN_TYPE, null: true, - description: 'Indicates whether the operation returns a record detected as spam' + description: 'Indicates whether the operation returns a record detected as spam.' end def with_spam_params(&block) diff --git a/app/graphql/mutations/container_expiration_policies/update.rb b/app/graphql/mutations/container_expiration_policies/update.rb index 4bff04bb705..37cf2fa6bf3 100644 --- a/app/graphql/mutations/container_expiration_policies/update.rb +++ b/app/graphql/mutations/container_expiration_policies/update.rb @@ -12,7 +12,7 @@ module Mutations argument :project_path, GraphQL::ID_TYPE, required: true, - description: 'The project path where the container expiration policy is located' + description: 'The project path where the container expiration policy is located.' argument :enabled, GraphQL::BOOLEAN_TYPE, @@ -47,7 +47,7 @@ module Mutations field :container_expiration_policy, Types::ContainerExpirationPolicyType, null: true, - description: 'The container expiration policy after mutation' + description: 'The container expiration policy after mutation.' def resolve(project_path:, **args) project = authorized_find!(full_path: project_path) diff --git a/app/graphql/mutations/container_repositories/destroy_tags.rb b/app/graphql/mutations/container_repositories/destroy_tags.rb index ca6a67867c3..636ceccee04 100644 --- a/app/graphql/mutations/container_repositories/destroy_tags.rb +++ b/app/graphql/mutations/container_repositories/destroy_tags.rb @@ -28,7 +28,7 @@ module Mutations field :deleted_tag_names, [GraphQL::STRING_TYPE], - description: 'Deleted container repository tags', + description: 'Deleted container repository tags.', null: false def resolve(id:, tag_names:) diff --git a/app/graphql/mutations/custom_emoji/create.rb b/app/graphql/mutations/custom_emoji/create.rb index d912a29d12e..9ec96be0f26 100644 --- a/app/graphql/mutations/custom_emoji/create.rb +++ b/app/graphql/mutations/custom_emoji/create.rb @@ -12,20 +12,20 @@ module Mutations field :custom_emoji, Types::CustomEmojiType, null: true, - description: 'The new custom emoji' + description: 'The new custom emoji.' argument :group_path, GraphQL::ID_TYPE, required: true, - description: 'Namespace full path the emoji is associated with' + description: 'Namespace full path the emoji is associated with.' argument :name, GraphQL::STRING_TYPE, required: true, - description: 'Name of the emoji' + description: 'Name of the emoji.' argument :url, GraphQL::STRING_TYPE, required: true, as: :file, - description: 'Location of the emoji file' + description: 'Location of the emoji file.' def resolve(group_path:, **args) group = authorized_find!(group_path: group_path) diff --git a/app/graphql/mutations/design_management/base.rb b/app/graphql/mutations/design_management/base.rb index 69fd22e46cd..14d85885793 100644 --- a/app/graphql/mutations/design_management/base.rb +++ b/app/graphql/mutations/design_management/base.rb @@ -7,11 +7,11 @@ module Mutations argument :project_path, GraphQL::ID_TYPE, required: true, - description: "The project where the issue is to upload designs for" + description: "The project where the issue is to upload designs for." argument :iid, GraphQL::ID_TYPE, required: true, - description: "The IID of the issue to modify designs for" + description: "The IID of the issue to modify designs for." private diff --git a/app/graphql/mutations/design_management/delete.rb b/app/graphql/mutations/design_management/delete.rb index d2ef2c9bcca..f604542edef 100644 --- a/app/graphql/mutations/design_management/delete.rb +++ b/app/graphql/mutations/design_management/delete.rb @@ -9,14 +9,14 @@ module Mutations argument :filenames, [GraphQL::STRING_TYPE], required: true, - description: "The filenames of the designs to delete", + description: "The filenames of the designs to delete.", prepare: ->(names, _ctx) do names.presence || (raise Errors::ArgumentError, 'no filenames') end field :version, Types::DesignManagement::VersionType, null: true, # null on error - description: 'The new version in which the designs are deleted' + description: 'The new version in which the designs are deleted.' authorize :destroy_design diff --git a/app/graphql/mutations/design_management/move.rb b/app/graphql/mutations/design_management/move.rb index aed4cfec0fd..fe280e926d2 100644 --- a/app/graphql/mutations/design_management/move.rb +++ b/app/graphql/mutations/design_management/move.rb @@ -8,17 +8,17 @@ module Mutations DesignID = ::Types::GlobalIDType[::DesignManagement::Design] argument :id, DesignID, required: true, as: :current_design, - description: "ID of the design to move" + description: "ID of the design to move." argument :previous, DesignID, required: false, as: :previous_design, - description: "ID of the immediately preceding design" + description: "ID of the immediately preceding design." argument :next, DesignID, required: false, as: :next_design, - description: "ID of the immediately following design" + description: "ID of the immediately following design." field :design_collection, Types::DesignManagement::DesignCollectionType, null: true, - description: "The current state of the collection" + description: "The current state of the collection." def resolve(**args) service = ::DesignManagement::MoveDesignsService.new(current_user, parameters(**args)) diff --git a/app/graphql/mutations/design_management/upload.rb b/app/graphql/mutations/design_management/upload.rb index 1ed7f8e49e6..2ccf2ef8ff5 100644 --- a/app/graphql/mutations/design_management/upload.rb +++ b/app/graphql/mutations/design_management/upload.rb @@ -7,13 +7,13 @@ module Mutations argument :files, [ApolloUploadServer::Upload], required: true, - description: "The files to upload" + description: "The files to upload." authorize :create_design field :designs, [Types::DesignManagement::DesignType], null: false, - description: "The designs that were uploaded by the mutation" + description: "The designs that were uploaded by the mutation." field :skipped_designs, [Types::DesignManagement::DesignType], null: false, diff --git a/app/graphql/mutations/discussions/toggle_resolve.rb b/app/graphql/mutations/discussions/toggle_resolve.rb index 0e3baf8d548..c9834c946b2 100644 --- a/app/graphql/mutations/discussions/toggle_resolve.rb +++ b/app/graphql/mutations/discussions/toggle_resolve.rb @@ -10,17 +10,17 @@ module Mutations argument :id, Types::GlobalIDType[Discussion], required: true, - description: 'The global ID of the discussion' + description: 'The global ID of the discussion.' argument :resolve, GraphQL::BOOLEAN_TYPE, required: true, - description: 'Will resolve the discussion when true, and unresolve the discussion when false' + description: 'Will resolve the discussion when true, and unresolve the discussion when false.' field :discussion, Types::Notes::DiscussionType, null: true, - description: 'The discussion after mutation' + description: 'The discussion after mutation.' def resolve(id:, resolve:) discussion = authorized_find_discussion!(id: id) diff --git a/app/graphql/mutations/environments/canary_ingress/update.rb b/app/graphql/mutations/environments/canary_ingress/update.rb index 1798143053a..45dcc8314a0 100644 --- a/app/graphql/mutations/environments/canary_ingress/update.rb +++ b/app/graphql/mutations/environments/canary_ingress/update.rb @@ -11,12 +11,12 @@ module Mutations argument :id, ::Types::GlobalIDType[::Environment], required: true, - description: 'The global ID of the environment to update' + description: 'The global ID of the environment to update.' argument :weight, GraphQL::INT_TYPE, required: true, - description: 'The weight of the Canary Ingress' + description: 'The weight of the Canary Ingress.' def resolve(id:, **kwargs) environment = authorized_find!(id: id) diff --git a/app/graphql/mutations/issues/base.rb b/app/graphql/mutations/issues/base.rb index 529d48f3cd0..b25987a43f6 100644 --- a/app/graphql/mutations/issues/base.rb +++ b/app/graphql/mutations/issues/base.rb @@ -7,16 +7,16 @@ module Mutations argument :project_path, GraphQL::ID_TYPE, required: true, - description: "The project the issue to mutate is in" + description: "The project the issue to mutate is in." argument :iid, GraphQL::STRING_TYPE, required: true, - description: "The IID of the issue to mutate" + description: "The IID of the issue to mutate." field :issue, Types::IssueType, null: true, - description: "The issue after mutation" + description: "The issue after mutation." authorize :update_issue diff --git a/app/graphql/mutations/issues/create.rb b/app/graphql/mutations/issues/create.rb index 1454916bc77..18b80ff1736 100644 --- a/app/graphql/mutations/issues/create.rb +++ b/app/graphql/mutations/issues/create.rb @@ -12,11 +12,11 @@ module Mutations argument :project_path, GraphQL::ID_TYPE, required: true, - description: 'Project full path the issue is associated with' + description: 'Project full path the issue is associated with.' argument :iid, GraphQL::INT_TYPE, required: false, - description: 'The IID (internal ID) of a project issue. Only admins and project owners can modify' + description: 'The IID (internal ID) of a project issue. Only admins and project owners can modify.' argument :title, GraphQL::STRING_TYPE, required: true, @@ -24,7 +24,7 @@ module Mutations argument :milestone_id, ::Types::GlobalIDType[::Milestone], required: false, - description: 'The ID of the milestone to assign to the issue. On update milestone will be removed if set to null' + description: 'The ID of the milestone to assign to the issue. On update milestone will be removed if set to null.' argument :labels, [GraphQL::STRING_TYPE], required: false, @@ -32,28 +32,28 @@ module Mutations argument :label_ids, [::Types::GlobalIDType[::Label]], required: false, - description: 'The IDs of labels to be added to the issue' + description: 'The IDs of labels to be added to the issue.' argument :created_at, Types::TimeType, required: false, - description: 'Timestamp when the issue was created. Available only for admins and project owners' + description: 'Timestamp when the issue was created. Available only for admins and project owners.' argument :merge_request_to_resolve_discussions_of, ::Types::GlobalIDType[::MergeRequest], required: false, - description: 'The IID of a merge request for which to resolve discussions' + description: 'The IID of a merge request for which to resolve discussions.' argument :discussion_to_resolve, GraphQL::STRING_TYPE, required: false, - description: 'The ID of a discussion to resolve. Also pass `merge_request_to_resolve_discussions_of`' + description: 'The ID of a discussion to resolve. Also pass `merge_request_to_resolve_discussions_of`.' argument :assignee_ids, [::Types::GlobalIDType[::User]], required: false, - description: 'The array of user IDs to assign to the issue' + description: 'The array of user IDs to assign to the issue.' field :issue, Types::IssueType, null: true, - description: 'The issue after mutation' + description: 'The issue after mutation.' def ready?(**args) if args.slice(*mutually_exclusive_label_args).size > 1 diff --git a/app/graphql/mutations/issues/move.rb b/app/graphql/mutations/issues/move.rb index e6971c9df8c..3f97325c921 100644 --- a/app/graphql/mutations/issues/move.rb +++ b/app/graphql/mutations/issues/move.rb @@ -8,7 +8,7 @@ module Mutations argument :target_project_path, GraphQL::ID_TYPE, required: true, - description: 'The project to move the issue to' + description: 'The project to move the issue to.' def resolve(project_path:, iid:, target_project_path:) Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/-/issues/267762') diff --git a/app/graphql/mutations/issues/set_due_date.rb b/app/graphql/mutations/issues/set_due_date.rb index effd863c541..da7892f4ed4 100644 --- a/app/graphql/mutations/issues/set_due_date.rb +++ b/app/graphql/mutations/issues/set_due_date.rb @@ -8,7 +8,7 @@ module Mutations argument :due_date, Types::TimeType, required: true, - description: 'The desired due date for the issue' + description: 'The desired due date for the issue.' def resolve(project_path:, iid:, due_date:) issue = authorized_find!(project_path: project_path, iid: iid) diff --git a/app/graphql/mutations/issues/set_locked.rb b/app/graphql/mutations/issues/set_locked.rb index 63a8483067a..611226e48ad 100644 --- a/app/graphql/mutations/issues/set_locked.rb +++ b/app/graphql/mutations/issues/set_locked.rb @@ -8,7 +8,7 @@ module Mutations argument :locked, GraphQL::BOOLEAN_TYPE, required: true, - description: 'Whether or not to lock discussion on the issue' + description: 'Whether or not to lock discussion on the issue.' def resolve(project_path:, iid:, locked:) issue = authorized_find!(project_path: project_path, iid: iid) diff --git a/app/graphql/mutations/issues/update.rb b/app/graphql/mutations/issues/update.rb index d34e351b2a6..eea2cd49aa0 100644 --- a/app/graphql/mutations/issues/update.rb +++ b/app/graphql/mutations/issues/update.rb @@ -13,18 +13,18 @@ module Mutations argument :milestone_id, GraphQL::ID_TYPE, # rubocop: disable Graphql/IDType required: false, - description: 'The ID of the milestone to assign to the issue. On update milestone will be removed if set to null' + description: 'The ID of the milestone to assign to the issue. On update milestone will be removed if set to null.' argument :add_label_ids, [GraphQL::ID_TYPE], required: false, - description: 'The IDs of labels to be added to the issue' + description: 'The IDs of labels to be added to the issue.' argument :remove_label_ids, [GraphQL::ID_TYPE], required: false, - description: 'The IDs of labels to be removed from the issue' + description: 'The IDs of labels to be removed from the issue.' argument :state_event, Types::IssueStateEventEnum, - description: 'Close or reopen an issue', + description: 'Close or reopen an issue.', required: false def resolve(project_path:, iid:, **args) diff --git a/app/graphql/mutations/jira_import/import_users.rb b/app/graphql/mutations/jira_import/import_users.rb index 0d59537b903..616ef390657 100644 --- a/app/graphql/mutations/jira_import/import_users.rb +++ b/app/graphql/mutations/jira_import/import_users.rb @@ -14,10 +14,10 @@ module Mutations argument :project_path, GraphQL::ID_TYPE, required: true, - description: 'The project to import the Jira users into' + description: 'The project to import the Jira users into.' argument :start_at, GraphQL::INT_TYPE, required: false, - description: 'The index of the record the import should started at, default 0 (50 records returned)' + description: 'The index of the record the import should started at, default 0 (50 records returned).' def resolve(project_path:, start_at: 0) project = authorized_find!(full_path: project_path) diff --git a/app/graphql/mutations/jira_import/start.rb b/app/graphql/mutations/jira_import/start.rb index eda28059272..3d50ebde13a 100644 --- a/app/graphql/mutations/jira_import/start.rb +++ b/app/graphql/mutations/jira_import/start.rb @@ -10,21 +10,21 @@ module Mutations field :jira_import, Types::JiraImportType, null: true, - description: 'The Jira import data after mutation' + description: 'The Jira import data after mutation.' argument :project_path, GraphQL::ID_TYPE, required: true, - description: 'The project to import the Jira project into' + description: 'The project to import the Jira project into.' argument :jira_project_key, GraphQL::STRING_TYPE, required: true, - description: 'Project key of the importer Jira project' + description: 'Project key of the importer Jira project.' argument :jira_project_name, GraphQL::STRING_TYPE, required: false, - description: 'Project name of the importer Jira project' + description: 'Project name of the importer Jira project.' argument :users_mapping, [Types::JiraUsersMappingInputType], required: false, - description: 'The mapping of Jira to GitLab users' + description: 'The mapping of Jira to GitLab users.' def resolve(project_path:, jira_project_key:, users_mapping:) project = authorized_find!(full_path: project_path) diff --git a/app/graphql/mutations/labels/create.rb b/app/graphql/mutations/labels/create.rb index cb03651618e..ccbd1c37cbf 100644 --- a/app/graphql/mutations/labels/create.rb +++ b/app/graphql/mutations/labels/create.rb @@ -10,20 +10,20 @@ module Mutations field :label, Types::LabelType, null: true, - description: 'The label after mutation' + description: 'The label after mutation.' argument :title, GraphQL::STRING_TYPE, required: true, - description: 'Title of the label' + description: 'Title of the label.' argument :description, GraphQL::STRING_TYPE, required: false, - description: 'Description of the label' + description: 'Description of the label.' argument :color, GraphQL::STRING_TYPE, required: false, default_value: Label::DEFAULT_COLOR, - description: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the CSS color names in https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords" + description: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the CSS color names in https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords." authorize :admin_label diff --git a/app/graphql/mutations/merge_requests/base.rb b/app/graphql/mutations/merge_requests/base.rb index 57920259cf7..cd919a19ba2 100644 --- a/app/graphql/mutations/merge_requests/base.rb +++ b/app/graphql/mutations/merge_requests/base.rb @@ -7,16 +7,16 @@ module Mutations argument :project_path, GraphQL::ID_TYPE, required: true, - description: "The project the merge request to mutate is in" + description: "The project the merge request to mutate is in." argument :iid, GraphQL::STRING_TYPE, required: true, - description: "The IID of the merge request to mutate" + description: "The IID of the merge request to mutate." field :merge_request, Types::MergeRequestType, null: true, - description: "The merge request after mutation" + description: "The merge request after mutation." authorize :update_merge_request diff --git a/app/graphql/mutations/merge_requests/create.rb b/app/graphql/mutations/merge_requests/create.rb index fd2cd58a5ee..64fa8417e50 100644 --- a/app/graphql/mutations/merge_requests/create.rb +++ b/app/graphql/mutations/merge_requests/create.rb @@ -9,7 +9,7 @@ module Mutations argument :project_path, GraphQL::ID_TYPE, required: true, - description: 'Project full path the merge request is associated with' + description: 'Project full path the merge request is associated with.' argument :title, GraphQL::STRING_TYPE, required: true, @@ -34,7 +34,7 @@ module Mutations field :merge_request, Types::MergeRequestType, null: true, - description: 'The merge request after mutation' + description: 'The merge request after mutation.' authorize :create_merge_request_from diff --git a/app/graphql/mutations/merge_requests/update.rb b/app/graphql/mutations/merge_requests/update.rb index b583fdfca9b..4721ebab41b 100644 --- a/app/graphql/mutations/merge_requests/update.rb +++ b/app/graphql/mutations/merge_requests/update.rb @@ -20,7 +20,7 @@ module Mutations description: copy_field_description(Types::MergeRequestType, :description) def resolve(args) - merge_request = authorized_find!(args.slice(:project_path, :iid)) + merge_request = authorized_find!(**args.slice(:project_path, :iid)) attributes = args.slice(:title, :description, :target_branch).compact ::MergeRequests::UpdateService diff --git a/app/graphql/mutations/metrics/dashboard/annotations/create.rb b/app/graphql/mutations/metrics/dashboard/annotations/create.rb index c2ec88c68ed..85937809eb8 100644 --- a/app/graphql/mutations/metrics/dashboard/annotations/create.rb +++ b/app/graphql/mutations/metrics/dashboard/annotations/create.rb @@ -15,35 +15,35 @@ module Mutations field :annotation, Types::Metrics::Dashboards::AnnotationType, null: true, - description: 'The created annotation' + description: 'The created annotation.' argument :environment_id, ::Types::GlobalIDType[::Environment], required: false, - description: 'The global ID of the environment to add an annotation to' + description: 'The global ID of the environment to add an annotation to.' argument :cluster_id, ::Types::GlobalIDType[::Clusters::Cluster], required: false, - description: 'The global ID of the cluster to add an annotation to' + description: 'The global ID of the cluster to add an annotation to.' argument :starting_at, Types::TimeType, required: true, - description: 'Timestamp indicating starting moment to which the annotation relates' + description: 'Timestamp indicating starting moment to which the annotation relates.' argument :ending_at, Types::TimeType, required: false, - description: 'Timestamp indicating ending moment to which the annotation relates' + description: 'Timestamp indicating ending moment to which the annotation relates.' argument :dashboard_path, GraphQL::STRING_TYPE, required: true, - description: 'The path to a file defining the dashboard on which the annotation should be added' + description: 'The path to a file defining the dashboard on which the annotation should be added.' argument :description, GraphQL::STRING_TYPE, required: true, - description: 'The description of the annotation' + description: 'The description of the annotation.' AnnotationSource = Struct.new(:object, keyword_init: true) do def type_keys diff --git a/app/graphql/mutations/metrics/dashboard/annotations/delete.rb b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb index 5d6763d8711..e0fadff13d4 100644 --- a/app/graphql/mutations/metrics/dashboard/annotations/delete.rb +++ b/app/graphql/mutations/metrics/dashboard/annotations/delete.rb @@ -11,7 +11,7 @@ module Mutations argument :id, ::Types::GlobalIDType[::Metrics::Dashboard::Annotation], required: true, - description: 'Global ID of the annotation to delete' + description: 'Global ID of the annotation to delete.' def resolve(id:) annotation = authorized_find!(id: id) diff --git a/app/graphql/mutations/namespace/package_settings/update.rb b/app/graphql/mutations/namespace/package_settings/update.rb new file mode 100644 index 00000000000..ca21c3418fc --- /dev/null +++ b/app/graphql/mutations/namespace/package_settings/update.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Mutations + module Namespace + module PackageSettings + class Update < Mutations::BaseMutation + include Mutations::ResolvesNamespace + + graphql_name 'UpdateNamespacePackageSettings' + + authorize :create_package_settings + + argument :namespace_path, + GraphQL::ID_TYPE, + required: true, + description: 'The namespace path where the namespace package setting is located.' + + argument :maven_duplicates_allowed, + GraphQL::BOOLEAN_TYPE, + required: false, + description: copy_field_description(Types::Namespace::PackageSettingsType, :maven_duplicates_allowed) + + argument :maven_duplicate_exception_regex, + Types::UntrustedRegexp, + required: false, + description: copy_field_description(Types::Namespace::PackageSettingsType, :maven_duplicate_exception_regex) + + field :package_settings, + Types::Namespace::PackageSettingsType, + null: true, + description: 'The namespace package setting after mutation.' + + def resolve(namespace_path:, **args) + namespace = authorized_find!(namespace_path: namespace_path) + + result = ::Namespaces::PackageSettings::UpdateService + .new(container: namespace, current_user: current_user, params: args) + .execute + + { + package_settings: result.payload[:package_settings], + errors: result.errors + } + end + + private + + def find_object(namespace_path:) + resolve_namespace(full_path: namespace_path) + end + end + end + end +end diff --git a/app/graphql/mutations/notes/base.rb b/app/graphql/mutations/notes/base.rb index f2678211335..ff401167ba1 100644 --- a/app/graphql/mutations/notes/base.rb +++ b/app/graphql/mutations/notes/base.rb @@ -6,7 +6,7 @@ module Mutations field :note, Types::Notes::NoteType, null: true, - description: 'The note after mutation' + description: 'The note after mutation.' private diff --git a/app/graphql/mutations/notes/create/base.rb b/app/graphql/mutations/notes/create/base.rb index a1d81c62d91..2351af01813 100644 --- a/app/graphql/mutations/notes/create/base.rb +++ b/app/graphql/mutations/notes/create/base.rb @@ -11,7 +11,7 @@ module Mutations argument :noteable_id, ::Types::GlobalIDType[::Noteable], required: true, - description: 'The global ID of the resource to add a note to' + description: 'The global ID of the resource to add a note to.' argument :body, GraphQL::STRING_TYPE, diff --git a/app/graphql/mutations/notes/create/note.rb b/app/graphql/mutations/notes/create/note.rb index f1cd3bddca8..7af93521e0d 100644 --- a/app/graphql/mutations/notes/create/note.rb +++ b/app/graphql/mutations/notes/create/note.rb @@ -9,7 +9,7 @@ module Mutations argument :discussion_id, ::Types::GlobalIDType[::Discussion], required: false, - description: 'The global ID of the discussion this note is in reply to' + description: 'The global ID of the discussion this note is in reply to.' private diff --git a/app/graphql/mutations/notes/destroy.rb b/app/graphql/mutations/notes/destroy.rb index 0e6a215bf00..21b2cd15a7e 100644 --- a/app/graphql/mutations/notes/destroy.rb +++ b/app/graphql/mutations/notes/destroy.rb @@ -10,7 +10,7 @@ module Mutations argument :id, ::Types::GlobalIDType[::Note], required: true, - description: 'The global ID of the note to destroy' + description: 'The global ID of the note to destroy.' def resolve(id:) note = authorized_find!(id: id) diff --git a/app/graphql/mutations/notes/reposition_image_diff_note.rb b/app/graphql/mutations/notes/reposition_image_diff_note.rb index 15bfb361b13..31d3b7c9bb0 100644 --- a/app/graphql/mutations/notes/reposition_image_diff_note.rb +++ b/app/graphql/mutations/notes/reposition_image_diff_note.rb @@ -16,7 +16,7 @@ module Mutations loads: Types::Notes::NoteType, as: :note, required: true, - description: 'The global ID of the DiffNote to update' + description: 'The global ID of the DiffNote to update.' argument :position, Types::Notes::UpdateDiffImagePositionInputType, diff --git a/app/graphql/mutations/notes/update/base.rb b/app/graphql/mutations/notes/update/base.rb index 42dac20f5d3..4edb7429b97 100644 --- a/app/graphql/mutations/notes/update/base.rb +++ b/app/graphql/mutations/notes/update/base.rb @@ -11,7 +11,7 @@ module Mutations argument :id, ::Types::GlobalIDType[::Note], required: true, - description: 'The global ID of the note to update' + description: 'The global ID of the note to update.' def resolve(args) note = authorized_find!(id: args[:id]) diff --git a/app/graphql/mutations/releases/base.rb b/app/graphql/mutations/releases/base.rb index d53cfbe6a11..dd1724fe320 100644 --- a/app/graphql/mutations/releases/base.rb +++ b/app/graphql/mutations/releases/base.rb @@ -7,7 +7,7 @@ module Mutations argument :project_path, GraphQL::ID_TYPE, required: true, - description: 'Full path of the project the release is associated with' + description: 'Full path of the project the release is associated with.' private diff --git a/app/graphql/mutations/releases/create.rb b/app/graphql/mutations/releases/create.rb index 156cd252848..91ac256033e 100644 --- a/app/graphql/mutations/releases/create.rb +++ b/app/graphql/mutations/releases/create.rb @@ -8,23 +8,23 @@ module Mutations field :release, Types::ReleaseType, null: true, - description: 'The release after mutation' + description: 'The release after mutation.' argument :tag_name, GraphQL::STRING_TYPE, required: true, as: :tag, - description: 'Name of the tag to associate with the release' + description: 'Name of the tag to associate with the release.' argument :ref, GraphQL::STRING_TYPE, required: false, - description: 'The commit SHA or branch name to use if creating a new tag' + description: 'The commit SHA or branch name to use if creating a new tag.' argument :name, GraphQL::STRING_TYPE, required: false, - description: 'Name of the release' + description: 'Name of the release.' argument :description, GraphQL::STRING_TYPE, required: false, - description: 'Description (also known as "release notes") of the release' + description: 'Description (also known as "release notes") of the release.' argument :released_at, Types::TimeType, required: false, @@ -36,7 +36,7 @@ module Mutations argument :assets, Types::ReleaseAssetsInputType, required: false, - description: 'Assets associated to the release' + description: 'Assets associated to the release.' authorize :create_release diff --git a/app/graphql/mutations/releases/update.rb b/app/graphql/mutations/releases/update.rb index bf72b907679..dff743254bd 100644 --- a/app/graphql/mutations/releases/update.rb +++ b/app/graphql/mutations/releases/update.rb @@ -12,19 +12,19 @@ module Mutations argument :tag_name, GraphQL::STRING_TYPE, required: true, as: :tag, - description: 'Name of the tag associated with the release' + description: 'Name of the tag associated with the release.' argument :name, GraphQL::STRING_TYPE, required: false, - description: 'Name of the release' + description: 'Name of the release.' argument :description, GraphQL::STRING_TYPE, required: false, - description: 'Description (release notes) of the release' + description: 'Description (release notes) of the release.' argument :released_at, Types::TimeType, required: false, - description: 'The release date' + description: 'The release date.' argument :milestones, [GraphQL::STRING_TYPE], required: false, @@ -51,17 +51,17 @@ module Mutations params = scalars.with_indifferent_access - release_result = ::Releases::UpdateService.new(project, current_user, params).execute + result = ::Releases::UpdateService.new(project, current_user, params).execute - if release_result[:status] == :success + if result[:status] == :success { - release: release_result[:release], + release: result[:release], errors: [] } else { release: nil, - errors: [release_result[:message]] + errors: [result[:message]] } end end diff --git a/app/graphql/mutations/snippets/base.rb b/app/graphql/mutations/snippets/base.rb index 023f876d035..5196bc5c7ed 100644 --- a/app/graphql/mutations/snippets/base.rb +++ b/app/graphql/mutations/snippets/base.rb @@ -6,7 +6,7 @@ module Mutations field :snippet, Types::SnippetType, null: true, - description: 'The snippet after mutation' + description: 'The snippet after mutation.' private diff --git a/app/graphql/mutations/snippets/create.rb b/app/graphql/mutations/snippets/create.rb index 56c3b398949..b4485e28c5a 100644 --- a/app/graphql/mutations/snippets/create.rb +++ b/app/graphql/mutations/snippets/create.rb @@ -12,30 +12,30 @@ module Mutations field :snippet, Types::SnippetType, null: true, - description: 'The snippet after mutation' + description: 'The snippet after mutation.' argument :title, GraphQL::STRING_TYPE, required: true, - description: 'Title of the snippet' + description: 'Title of the snippet.' argument :description, GraphQL::STRING_TYPE, required: false, - description: 'Description of the snippet' + description: 'Description of the snippet.' argument :visibility_level, Types::VisibilityLevelsEnum, - description: 'The visibility level of the snippet', + description: 'The visibility level of the snippet.', required: true argument :project_path, GraphQL::ID_TYPE, required: false, - description: 'The project full path the snippet is associated with' + description: 'The project full path the snippet is associated with.' argument :uploaded_files, [GraphQL::STRING_TYPE], required: false, - description: 'The paths to files uploaded in the snippet description' + description: 'The paths to files uploaded in the snippet description.' argument :blob_actions, [Types::Snippets::BlobActionInputType], - description: 'Actions to perform over the snippet repository and blobs', + description: 'Actions to perform over the snippet repository and blobs.', required: false def resolve(project_path: nil, **args) diff --git a/app/graphql/mutations/snippets/destroy.rb b/app/graphql/mutations/snippets/destroy.rb index bee6503372d..9b00f62e2f9 100644 --- a/app/graphql/mutations/snippets/destroy.rb +++ b/app/graphql/mutations/snippets/destroy.rb @@ -9,7 +9,7 @@ module Mutations argument :id, ::Types::GlobalIDType[::Snippet], required: true, - description: 'The global ID of the snippet to destroy' + description: 'The global ID of the snippet to destroy.' def resolve(id:) snippet = authorized_find!(id: id) diff --git a/app/graphql/mutations/snippets/mark_as_spam.rb b/app/graphql/mutations/snippets/mark_as_spam.rb index 2d6fea1f5ec..d6e3e131b81 100644 --- a/app/graphql/mutations/snippets/mark_as_spam.rb +++ b/app/graphql/mutations/snippets/mark_as_spam.rb @@ -7,7 +7,7 @@ module Mutations argument :id, ::Types::GlobalIDType[::Snippet], required: true, - description: 'The global ID of the snippet to update' + description: 'The global ID of the snippet to update.' def resolve(id:) snippet = authorized_find!(id: id) diff --git a/app/graphql/mutations/snippets/update.rb b/app/graphql/mutations/snippets/update.rb index 6df1ad6d8b9..930440fbd35 100644 --- a/app/graphql/mutations/snippets/update.rb +++ b/app/graphql/mutations/snippets/update.rb @@ -9,22 +9,22 @@ module Mutations argument :id, ::Types::GlobalIDType[::Snippet], required: true, - description: 'The global ID of the snippet to update' + description: 'The global ID of the snippet to update.' argument :title, GraphQL::STRING_TYPE, required: false, - description: 'Title of the snippet' + description: 'Title of the snippet.' argument :description, GraphQL::STRING_TYPE, required: false, - description: 'Description of the snippet' + description: 'Description of the snippet.' argument :visibility_level, Types::VisibilityLevelsEnum, - description: 'The visibility level of the snippet', + description: 'The visibility level of the snippet.', required: false argument :blob_actions, [Types::Snippets::BlobActionInputType], - description: 'Actions to perform over the snippet repository and blobs', + description: 'Actions to perform over the snippet repository and blobs.', required: false def resolve(id:, **args) diff --git a/app/graphql/mutations/terraform/state/base.rb b/app/graphql/mutations/terraform/state/base.rb index b1721c784b1..01f69934ea3 100644 --- a/app/graphql/mutations/terraform/state/base.rb +++ b/app/graphql/mutations/terraform/state/base.rb @@ -9,7 +9,7 @@ module Mutations argument :id, Types::GlobalIDType[::Terraform::State], required: true, - description: 'Global ID of the Terraform state' + description: 'Global ID of the Terraform state.' private diff --git a/app/graphql/mutations/todos/create.rb b/app/graphql/mutations/todos/create.rb index 53c88696fdd..814f7ec4fc4 100644 --- a/app/graphql/mutations/todos/create.rb +++ b/app/graphql/mutations/todos/create.rb @@ -10,11 +10,11 @@ module Mutations argument :target_id, Types::GlobalIDType[Todoable], required: true, - description: "The global ID of the to-do item's parent. Issues, merge requests, designs and epics are supported" + description: "The global ID of the to-do item's parent. Issues, merge requests, designs and epics are supported." field :todo, Types::TodoType, null: true, - description: 'The to-do created' + description: 'The to-do created.' def resolve(target_id:) id = ::Types::GlobalIDType[Todoable].coerce_isolated_input(target_id) diff --git a/app/graphql/mutations/todos/mark_all_done.rb b/app/graphql/mutations/todos/mark_all_done.rb index 97bbbeeaa2f..c8359953567 100644 --- a/app/graphql/mutations/todos/mark_all_done.rb +++ b/app/graphql/mutations/todos/mark_all_done.rb @@ -11,11 +11,11 @@ module Mutations [::Types::GlobalIDType[::Todo]], null: false, deprecated: { reason: 'Use todos', milestone: '13.2' }, - description: 'Ids of the updated todos' + description: 'Ids of the updated todos.' field :todos, [::Types::TodoType], null: false, - description: 'Updated todos' + description: 'Updated todos.' def resolve authorize!(current_user) diff --git a/app/graphql/mutations/todos/mark_done.rb b/app/graphql/mutations/todos/mark_done.rb index 2ae50846108..95144abb040 100644 --- a/app/graphql/mutations/todos/mark_done.rb +++ b/app/graphql/mutations/todos/mark_done.rb @@ -10,11 +10,11 @@ module Mutations argument :id, ::Types::GlobalIDType[::Todo], required: true, - description: 'The global ID of the todo to mark as done' + description: 'The global ID of the todo to mark as done.' field :todo, Types::TodoType, null: false, - description: 'The requested todo' + description: 'The requested todo.' def resolve(id:) todo = authorized_find!(id: id) diff --git a/app/graphql/mutations/todos/restore.rb b/app/graphql/mutations/todos/restore.rb index c532b455a16..e496627aec2 100644 --- a/app/graphql/mutations/todos/restore.rb +++ b/app/graphql/mutations/todos/restore.rb @@ -10,11 +10,11 @@ module Mutations argument :id, ::Types::GlobalIDType[::Todo], required: true, - description: 'The global ID of the todo to restore' + description: 'The global ID of the todo to restore.' field :todo, Types::TodoType, null: false, - description: 'The requested todo' + description: 'The requested todo.' def resolve(id:) todo = authorized_find!(id: id) diff --git a/app/graphql/mutations/todos/restore_many.rb b/app/graphql/mutations/todos/restore_many.rb index 59965589856..9263c1d9afe 100644 --- a/app/graphql/mutations/todos/restore_many.rb +++ b/app/graphql/mutations/todos/restore_many.rb @@ -10,16 +10,16 @@ module Mutations argument :ids, [::Types::GlobalIDType[::Todo]], required: true, - description: 'The global IDs of the todos to restore (a maximum of 50 is supported at once)' + description: 'The global IDs of the todos to restore (a maximum of 50 is supported at once).' field :updated_ids, [::Types::GlobalIDType[Todo]], null: false, - description: 'The IDs of the updated todo items', + description: 'The IDs of the updated todo items.', deprecated: { reason: 'Use todos', milestone: '13.2' } field :todos, [::Types::TodoType], null: false, - description: 'Updated todos' + description: 'Updated todos.' def resolve(ids:) check_update_amount_limit!(ids) diff --git a/app/graphql/queries/container_registry/get_container_repositories.query.graphql b/app/graphql/queries/container_registry/get_container_repositories.query.graphql new file mode 100644 index 00000000000..6171233c446 --- /dev/null +++ b/app/graphql/queries/container_registry/get_container_repositories.query.graphql @@ -0,0 +1,60 @@ +query getProjectContainerRepositories( + $fullPath: ID! + $name: String + $first: Int + $last: Int + $after: String + $before: String + $isGroupPage: Boolean! +) { + project(fullPath: $fullPath) @skip(if: $isGroupPage) { + __typename + containerRepositoriesCount + containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) { + __typename + nodes { + id + name + path + status + location + canDelete + createdAt + expirationPolicyStartedAt + __typename + } + pageInfo { + __typename + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } + group(fullPath: $fullPath) @include(if: $isGroupPage) { + __typename + containerRepositoriesCount + containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) { + __typename + nodes { + id + name + path + status + location + canDelete + createdAt + expirationPolicyStartedAt + __typename + } + pageInfo { + __typename + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } +} diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_details.query.graphql b/app/graphql/queries/pipelines/get_pipeline_details.query.graphql index 25aede49631..92323923266 100644 --- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_details.query.graphql +++ b/app/graphql/queries/pipelines/get_pipeline_details.query.graphql @@ -1,11 +1,34 @@ -#import "../fragments/linked_pipelines.fragment.graphql" +fragment LinkedPipelineData on Pipeline { + __typename + id + iid + path + status: detailedStatus { + __typename + group + label + icon + } + sourceJob { + __typename + name + } + project { + __typename + name + fullPath + } +} query getPipelineDetails($projectPath: ID!, $iid: ID!) { project(fullPath: $projectPath) { + __typename pipeline(iid: $iid) { + __typename id iid downstream { + __typename nodes { ...LinkedPipelineData } @@ -14,18 +37,25 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) { ...LinkedPipelineData } stages { + __typename nodes { + __typename name status: detailedStatus { + __typename action { + __typename icon path title } } groups { + __typename nodes { + __typename status: detailedStatus { + __typename label group icon @@ -33,21 +63,27 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) { name size jobs { + __typename nodes { + __typename name scheduledAt needs { + __typename nodes { + __typename name } } status: detailedStatus { + __typename icon tooltip hasDetails detailsPath group action { + __typename buttonTitle icon path diff --git a/app/graphql/queries/snippet/snippet.query.graphql b/app/graphql/queries/snippet/snippet.query.graphql index 2205dc26642..ebfc135c51c 100644 --- a/app/graphql/queries/snippet/snippet.query.graphql +++ b/app/graphql/queries/snippet/snippet.query.graphql @@ -59,6 +59,11 @@ query GetSnippetQuery($ids: [SnippetID!]) { name username webUrl + status { + __typename + emoji + message + } } } } diff --git a/app/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver.rb b/app/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver.rb index 9bac9f222ab..9df07df24d8 100644 --- a/app/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver.rb +++ b/app/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver.rb @@ -11,15 +11,15 @@ module Resolvers argument :identifier, Types::Admin::Analytics::InstanceStatistics::MeasurementIdentifierEnum, required: true, - description: 'The type of measurement/statistics to retrieve' + description: 'The type of measurement/statistics to retrieve.' argument :recorded_after, Types::TimeType, required: false, - description: 'Measurement recorded after this date' + description: 'Measurement recorded after this date.' argument :recorded_before, Types::TimeType, required: false, - description: 'Measurement recorded before this date' + description: 'Measurement recorded before this date.' def resolve(identifier:, recorded_before: nil, recorded_after: nil) authorize! diff --git a/app/graphql/resolvers/alert_management/alert_resolver.rb b/app/graphql/resolvers/alert_management/alert_resolver.rb index b115bd80113..d60cabde62b 100644 --- a/app/graphql/resolvers/alert_management/alert_resolver.rb +++ b/app/graphql/resolvers/alert_management/alert_resolver.rb @@ -7,19 +7,19 @@ module Resolvers argument :iid, GraphQL::STRING_TYPE, required: false, - description: 'IID of the alert. For example, "1"' + description: 'IID of the alert. For example, "1".' argument :statuses, [Types::AlertManagement::StatusEnum], as: :status, required: false, - description: 'Alerts with the specified statues. For example, [TRIGGERED]' + description: 'Alerts with the specified statues. For example, [TRIGGERED].' argument :sort, Types::AlertManagement::AlertSortEnum, - description: 'Sort alerts by this criteria', + description: 'Sort alerts by this criteria.', required: false argument :domain, Types::AlertManagement::DomainFilterEnum, - description: 'Filter query for given domain', + description: 'Filter query for given domain.', required: true, default_value: 'operations' @@ -29,7 +29,7 @@ module Resolvers argument :assignee_username, GraphQL::STRING_TYPE, required: false, - description: 'Username of a user assigned to the issue' + description: 'Username of a user assigned to the issue.' type Types::AlertManagement::AlertType, null: true diff --git a/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb b/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb index 8fc0f9fd1ff..410f72cff84 100644 --- a/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb +++ b/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb @@ -11,7 +11,7 @@ module Resolvers argument :assignee_username, GraphQL::STRING_TYPE, required: false, - description: 'Username of a user assigned to the issue' + description: 'Username of a user assigned to the issue.' def resolve(**args) ::Gitlab::AlertManagement::AlertStatusCounts.new(context[:current_user], object, args) diff --git a/app/graphql/resolvers/board_list_issues_resolver.rb b/app/graphql/resolvers/board_list_issues_resolver.rb index 3e4a5a3cb70..29e66a59a15 100644 --- a/app/graphql/resolvers/board_list_issues_resolver.rb +++ b/app/graphql/resolvers/board_list_issues_resolver.rb @@ -6,7 +6,7 @@ module Resolvers argument :filters, Types::Boards::BoardIssueInputType, required: false, - description: 'Filters applied when selecting issues in the board list' + description: 'Filters applied when selecting issues in the board list.' type Types::IssueType, null: true diff --git a/app/graphql/resolvers/board_lists_resolver.rb b/app/graphql/resolvers/board_lists_resolver.rb index 35d938c50be..7bdff8ba61f 100644 --- a/app/graphql/resolvers/board_lists_resolver.rb +++ b/app/graphql/resolvers/board_lists_resolver.rb @@ -13,11 +13,11 @@ module Resolvers argument :id, Types::GlobalIDType[List], required: false, - description: 'Find a list by its global ID' + description: 'Find a list by its global ID.' argument :issue_filters, Types::Boards::BoardIssueInputType, required: false, - description: 'Filters applied when getting issue metadata in the board list' + description: 'Filters applied when getting issue metadata in the board list.' alias_method :board, :object diff --git a/app/graphql/resolvers/board_resolver.rb b/app/graphql/resolvers/board_resolver.rb index 517f4e514c9..582707cc1e4 100644 --- a/app/graphql/resolvers/board_resolver.rb +++ b/app/graphql/resolvers/board_resolver.rb @@ -8,7 +8,7 @@ module Resolvers argument :id, ::Types::GlobalIDType[::Board], required: true, - description: 'The board\'s ID' + description: 'The board\'s ID.' def resolve(id: nil) return unless parent diff --git a/app/graphql/resolvers/boards_resolver.rb b/app/graphql/resolvers/boards_resolver.rb index 42b6ce03118..cdb15dc8f37 100644 --- a/app/graphql/resolvers/boards_resolver.rb +++ b/app/graphql/resolvers/boards_resolver.rb @@ -6,7 +6,7 @@ module Resolvers argument :id, ::Types::GlobalIDType[::Board], required: false, - description: 'Find a board by its ID' + description: 'Find a board by its ID.' def resolve(id: nil) # The project or group could have been loaded in batch by `BatchLoader`. diff --git a/app/graphql/resolvers/ci/config_resolver.rb b/app/graphql/resolvers/ci/config_resolver.rb index d6e7c206691..72d3ae30d73 100644 --- a/app/graphql/resolvers/ci/config_resolver.rb +++ b/app/graphql/resolvers/ci/config_resolver.rb @@ -3,40 +3,64 @@ module Resolvers module Ci class ConfigResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + include ResolvesProject + type Types::Ci::Config::ConfigType, null: true + authorize :read_pipeline + + argument :project_path, GraphQL::ID_TYPE, + required: true, + description: 'The project of the CI config.' + argument :content, GraphQL::STRING_TYPE, required: true, - description: 'Contents of .gitlab-ci.yml' - - def resolve(content:) - result = ::Gitlab::Ci::YamlProcessor.new(content).execute - - response = if result.errors.empty? - { - status: :valid, - errors: [], - stages: make_stages(result.jobs) - } - else - { - status: :invalid, - errors: result.errors - } - end - - response.merge(merged_yaml: result.merged_yaml) + description: "Contents of '.gitlab-ci.yml'." + + argument :dry_run, GraphQL::BOOLEAN_TYPE, + required: false, + description: 'Run pipeline creation simulation, or only do static check.' + + def resolve(project_path:, content:, dry_run: false) + project = authorized_find!(project_path: project_path) + + result = ::Gitlab::Ci::Lint + .new(project: project, current_user: context[:current_user]) + .validate(content, dry_run: dry_run) + + if result.errors.empty? + { + status: :valid, + errors: [], + stages: make_stages(result.jobs) + } + else + { + status: :invalid, + errors: result.errors + } + end end private def make_jobs(config_jobs) - config_jobs.map do |job_name, job| + config_jobs.map do |job| { - name: job_name, + name: job[:name], stage: job[:stage], - group_name: CommitStatus.new(name: job_name).group_name, - needs: job.dig(:needs, :job) || [] + group_name: CommitStatus.new(name: job[:name]).group_name, + needs: job.dig(:needs) || [], + allow_failure: job[:allow_failure], + before_script: job[:before_script], + script: job[:script], + after_script: job[:after_script], + only: job[:only], + except: job[:except], + when: job[:when], + tags: job[:tag_list], + environment: job[:environment] } end end @@ -55,6 +79,10 @@ module Resolvers .group_by { |group| group[:stage] } .map { |name, groups| { name: name, groups: groups } } end + + def find_object(project_path:) + resolve_project(full_path: project_path) + end end end end diff --git a/app/graphql/resolvers/ci/jobs_resolver.rb b/app/graphql/resolvers/ci/jobs_resolver.rb index 2c4911748a5..dd565094017 100644 --- a/app/graphql/resolvers/ci/jobs_resolver.rb +++ b/app/graphql/resolvers/ci/jobs_resolver.rb @@ -9,7 +9,7 @@ module Resolvers argument :security_report_types, [Types::Security::ReportTypeEnum], required: false, - description: 'Filter jobs by the type of security report they produce' + description: 'Filter jobs by the type of security report they produce.' def resolve(security_report_types: []) if security_report_types.present? diff --git a/app/graphql/resolvers/ci/runner_setup_resolver.rb b/app/graphql/resolvers/ci/runner_setup_resolver.rb index f68d71174c3..ac2a56b89a7 100644 --- a/app/graphql/resolvers/ci/runner_setup_resolver.rb +++ b/app/graphql/resolvers/ci/runner_setup_resolver.rb @@ -7,19 +7,19 @@ module Resolvers argument :platform, GraphQL::STRING_TYPE, required: true, - description: 'Platform to generate the instructions for' + description: 'Platform to generate the instructions for.' argument :architecture, GraphQL::STRING_TYPE, required: true, - description: 'Architecture to generate the instructions for' + description: 'Architecture to generate the instructions for.' argument :project_id, ::Types::GlobalIDType[::Project], required: false, - description: 'Project to register the runner for' + description: 'Project to register the runner for.' argument :group_id, ::Types::GlobalIDType[::Group], required: false, - description: 'Group to register the runner for' + description: 'Group to register the runner for.' def resolve(platform:, architecture:, **args) instructions = Gitlab::Ci::RunnerInstructions.new( diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb index 4715b867ecb..84b0dafe213 100644 --- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb +++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb @@ -8,52 +8,52 @@ module IssueResolverArguments argument :iid, GraphQL::STRING_TYPE, required: false, - description: 'IID of the issue. For example, "1"' + description: 'IID of the issue. For example, "1".' argument :iids, [GraphQL::STRING_TYPE], required: false, - description: 'List of IIDs of issues. For example, [1, 2]' + description: 'List of IIDs of issues. For example, [1, 2].' argument :label_name, GraphQL::STRING_TYPE.to_list_type, required: false, - description: 'Labels applied to this issue' + description: 'Labels applied to this issue.' argument :milestone_title, GraphQL::STRING_TYPE.to_list_type, required: false, - description: 'Milestone applied to this issue' + description: 'Milestone applied to this issue.' argument :author_username, GraphQL::STRING_TYPE, required: false, - description: 'Username of the author of the issue' + description: 'Username of the author of the issue.' argument :assignee_username, GraphQL::STRING_TYPE, required: false, - description: 'Username of a user assigned to the issue' + description: 'Username of a user assigned to the issue.' argument :assignee_usernames, [GraphQL::STRING_TYPE], required: false, - description: 'Usernames of users assigned to the issue' + description: 'Usernames of users assigned to the issue.' argument :assignee_id, GraphQL::STRING_TYPE, required: false, - description: 'ID of a user assigned to the issues, "none" and "any" values are supported' + description: 'ID of a user assigned to the issues, "none" and "any" values are supported.' argument :created_before, Types::TimeType, required: false, - description: 'Issues created before this date' + description: 'Issues created before this date.' argument :created_after, Types::TimeType, required: false, - description: 'Issues created after this date' + description: 'Issues created after this date.' argument :updated_before, Types::TimeType, required: false, - description: 'Issues updated before this date' + description: 'Issues updated before this date.' argument :updated_after, Types::TimeType, required: false, - description: 'Issues updated after this date' + description: 'Issues updated after this date.' argument :closed_before, Types::TimeType, required: false, - description: 'Issues closed before this date' + description: 'Issues closed before this date.' argument :closed_after, Types::TimeType, required: false, - description: 'Issues closed after this date' + description: 'Issues closed after this date.' argument :search, GraphQL::STRING_TYPE, required: false, - description: 'Search query for issue title or description' + description: 'Search query for issue title or description.' argument :types, [Types::IssueTypeEnum], as: :issue_types, - description: 'Filter issues by the given issue types', + description: 'Filter issues by the given issue types.', required: false end diff --git a/app/graphql/resolvers/concerns/looks_ahead.rb b/app/graphql/resolvers/concerns/looks_ahead.rb index d468047b539..77a85edfba6 100644 --- a/app/graphql/resolvers/concerns/looks_ahead.rb +++ b/app/graphql/resolvers/concerns/looks_ahead.rb @@ -20,11 +20,11 @@ module LooksAhead includes = preloads.each.flat_map do |name, requirements| selection&.selects?(name) ? requirements : [] end - preloads = (unconditional_includes + includes).uniq + all_preloads = (unconditional_includes + includes).uniq - return query if preloads.empty? + return query if all_preloads.empty? - query.preload(*preloads) # rubocop: disable CodeReuse/ActiveRecord + query.preload(*all_preloads) # rubocop: disable CodeReuse/ActiveRecord end private diff --git a/app/graphql/resolvers/concerns/resolves_merge_requests.rb b/app/graphql/resolvers/concerns/resolves_merge_requests.rb index ab83476ddea..31444b0c592 100644 --- a/app/graphql/resolvers/concerns/resolves_merge_requests.rb +++ b/app/graphql/resolvers/concerns/resolves_merge_requests.rb @@ -40,6 +40,8 @@ module ResolvesMergeRequests def preloads { assignees: [:assignees], + reviewers: [:reviewers], + participants: MergeRequest.participant_includes, labels: [:labels], author: [:author], merged_at: [:metrics], @@ -47,6 +49,7 @@ module ResolvesMergeRequests diff_stats_summary: [:metrics], approved_by: [:approved_by_users], milestone: [:milestone], + security_auto_fix: [:author], head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }] } end diff --git a/app/graphql/resolvers/concerns/resolves_pipelines.rb b/app/graphql/resolvers/concerns/resolves_pipelines.rb index f061f5f1606..bbf33c0b5eb 100644 --- a/app/graphql/resolvers/concerns/resolves_pipelines.rb +++ b/app/graphql/resolvers/concerns/resolves_pipelines.rb @@ -8,15 +8,15 @@ module ResolvesPipelines argument :status, Types::Ci::PipelineStatusEnum, required: false, - description: "Filter pipelines by their status" + description: "Filter pipelines by their status." argument :ref, GraphQL::STRING_TYPE, required: false, - description: "Filter pipelines by the ref they are run for" + description: "Filter pipelines by the ref they are run for." argument :sha, GraphQL::STRING_TYPE, required: false, - description: "Filter pipelines by the sha of the commit they are run for" + description: "Filter pipelines by the sha of the commit they are run for." end class_methods do diff --git a/app/graphql/resolvers/concerns/resolves_snippets.rb b/app/graphql/resolvers/concerns/resolves_snippets.rb index 790ff4f774f..0bc38188b9a 100644 --- a/app/graphql/resolvers/concerns/resolves_snippets.rb +++ b/app/graphql/resolvers/concerns/resolves_snippets.rb @@ -8,11 +8,11 @@ module ResolvesSnippets argument :ids, [::Types::GlobalIDType[::Snippet]], required: false, - description: 'Array of global snippet ids, e.g., "gid://gitlab/ProjectSnippet/1"' + description: 'Array of global snippet ids, e.g., "gid://gitlab/ProjectSnippet/1".' argument :visibility, Types::Snippets::VisibilityScopesEnum, required: false, - description: 'The visibility of the snippet' + description: 'The visibility of the snippet.' end def resolve(**args) diff --git a/app/graphql/resolvers/concerns/time_frame_arguments.rb b/app/graphql/resolvers/concerns/time_frame_arguments.rb index 94bfe6f7f9f..6cac46a71d2 100644 --- a/app/graphql/resolvers/concerns/time_frame_arguments.rb +++ b/app/graphql/resolvers/concerns/time_frame_arguments.rb @@ -18,7 +18,7 @@ module TimeFrameArguments argument :timeframe, Types::TimeframeInputType, required: false, - description: 'List items overlapping the given timeframe' + description: 'List items overlapping the given timeframe.' end # TODO: remove when the start_date and end_date arguments are removed diff --git a/app/graphql/resolvers/container_repositories_resolver.rb b/app/graphql/resolvers/container_repositories_resolver.rb index b4b2893a3b8..8042a368e33 100644 --- a/app/graphql/resolvers/container_repositories_resolver.rb +++ b/app/graphql/resolvers/container_repositories_resolver.rb @@ -8,7 +8,7 @@ module Resolvers argument :name, GraphQL::STRING_TYPE, required: false, - description: 'Filter the container repositories by their name' + description: 'Filter the container repositories by their name.' def resolve(name: nil) ContainerRepositoriesFinder.new(user: current_user, subject: object, params: { name: name }) diff --git a/app/graphql/resolvers/design_management/design_at_version_resolver.rb b/app/graphql/resolvers/design_management/design_at_version_resolver.rb index 1b69efebe4e..533692e2b12 100644 --- a/app/graphql/resolvers/design_management/design_at_version_resolver.rb +++ b/app/graphql/resolvers/design_management/design_at_version_resolver.rb @@ -11,7 +11,7 @@ module Resolvers argument :id, ::Types::GlobalIDType[::DesignManagement::DesignAtVersion], required: true, - description: 'The Global ID of the design at this version' + description: 'The Global ID of the design at this version.' def resolve(id:) authorized_find!(id: id) diff --git a/app/graphql/resolvers/design_management/design_resolver.rb b/app/graphql/resolvers/design_management/design_resolver.rb index b60c14ca835..e640f57f04a 100644 --- a/app/graphql/resolvers/design_management/design_resolver.rb +++ b/app/graphql/resolvers/design_management/design_resolver.rb @@ -9,11 +9,11 @@ module Resolvers argument :id, ::Types::GlobalIDType[::DesignManagement::Design], required: false, - description: 'Find a design by its ID' + description: 'Find a design by its ID.' argument :filename, GraphQL::STRING_TYPE, required: false, - description: 'Find a design by its filename' + description: 'Find a design by its filename.' def resolve(filename: nil, id: nil) params = parse_args(filename, id) diff --git a/app/graphql/resolvers/design_management/designs_resolver.rb b/app/graphql/resolvers/design_management/designs_resolver.rb index c588142ea6b..c45e4d01f6e 100644 --- a/app/graphql/resolvers/design_management/designs_resolver.rb +++ b/app/graphql/resolvers/design_management/designs_resolver.rb @@ -10,10 +10,10 @@ module Resolvers argument :ids, [DesignID], required: false, - description: 'Filters designs by their ID' + description: 'Filters designs by their ID.' argument :filenames, [GraphQL::STRING_TYPE], required: false, - description: 'Filters designs by their filename' + description: 'Filters designs by their filename.' argument :at_version, VersionID, required: false, description: 'Filters designs to only those that existed at the version. ' \ diff --git a/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb b/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb index 49a4974bfbf..fea74cbfb0b 100644 --- a/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb +++ b/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb @@ -19,13 +19,13 @@ module Resolvers argument :id, DesignAtVersionID, required: false, as: :design_at_version_id, - description: 'The ID of the DesignAtVersion' + description: 'The ID of the DesignAtVersion.' argument :design_id, DesignID, required: false, - description: 'The ID of a specific design' + description: 'The ID of a specific design.' argument :filename, GraphQL::STRING_TYPE, required: false, - description: 'The filename of a specific design' + description: 'The filename of a specific design.' def self.single self diff --git a/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb b/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb index a129d8620d4..930b1b60d0e 100644 --- a/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb +++ b/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb @@ -15,11 +15,11 @@ module Resolvers argument :ids, [DesignID], required: false, - description: 'Filters designs by their ID' + description: 'Filters designs by their ID.' argument :filenames, [GraphQL::STRING_TYPE], required: false, - description: 'Filters designs by their filename' + description: 'Filters designs by their filename.' def self.single ::Resolvers::DesignManagement::Version::DesignAtVersionResolver diff --git a/app/graphql/resolvers/design_management/version_in_collection_resolver.rb b/app/graphql/resolvers/design_management/version_in_collection_resolver.rb index 7d20cfc2c8e..593974beb04 100644 --- a/app/graphql/resolvers/design_management/version_in_collection_resolver.rb +++ b/app/graphql/resolvers/design_management/version_in_collection_resolver.rb @@ -17,11 +17,11 @@ module Resolvers argument :sha, GraphQL::STRING_TYPE, required: false, - description: "The SHA256 of a specific version" + description: "The SHA256 of a specific version." argument :id, VersionID, as: :version_id, required: false, - description: 'The Global ID of the version' + description: 'The Global ID of the version.' def resolve(version_id: nil, sha: nil) # TODO: remove this line when the compatibility layer is removed diff --git a/app/graphql/resolvers/design_management/version_resolver.rb b/app/graphql/resolvers/design_management/version_resolver.rb index 1bc9c1a7cd6..2144e588208 100644 --- a/app/graphql/resolvers/design_management/version_resolver.rb +++ b/app/graphql/resolvers/design_management/version_resolver.rb @@ -11,7 +11,7 @@ module Resolvers argument :id, ::Types::GlobalIDType[::DesignManagement::Version], required: true, - description: 'The Global ID of the version' + description: 'The Global ID of the version.' def resolve(id:) authorized_find!(id: id) diff --git a/app/graphql/resolvers/design_management/versions_resolver.rb b/app/graphql/resolvers/design_management/versions_resolver.rb index 3c718a631db..619448cbc18 100644 --- a/app/graphql/resolvers/design_management/versions_resolver.rb +++ b/app/graphql/resolvers/design_management/versions_resolver.rb @@ -14,12 +14,12 @@ module Resolvers argument :earlier_or_equal_to_sha, GraphQL::STRING_TYPE, as: :sha, required: false, - description: 'The SHA256 of the most recent acceptable version' + description: 'The SHA256 of the most recent acceptable version.' argument :earlier_or_equal_to_id, VersionID, as: :id, required: false, - description: 'The Global ID of the most recent acceptable version' + description: 'The Global ID of the most recent acceptable version.' # This resolver has a custom singular resolver def self.single diff --git a/app/graphql/resolvers/echo_resolver.rb b/app/graphql/resolvers/echo_resolver.rb index 6b85b700712..0c7dad622cf 100644 --- a/app/graphql/resolvers/echo_resolver.rb +++ b/app/graphql/resolvers/echo_resolver.rb @@ -6,7 +6,7 @@ module Resolvers description 'Testing endpoint to validate the API with' argument :text, GraphQL::STRING_TYPE, required: true, - description: 'Text to echo back' + description: 'Text to echo back.' def resolve(text:) username = current_user&.username diff --git a/app/graphql/resolvers/environments_resolver.rb b/app/graphql/resolvers/environments_resolver.rb index 1b916a89796..ed3395d05aa 100644 --- a/app/graphql/resolvers/environments_resolver.rb +++ b/app/graphql/resolvers/environments_resolver.rb @@ -4,15 +4,15 @@ module Resolvers class EnvironmentsResolver < BaseResolver argument :name, GraphQL::STRING_TYPE, required: false, - description: 'Name of the environment' + description: 'Name of the environment.' argument :search, GraphQL::STRING_TYPE, required: false, - description: 'Search query for environment name' + description: 'Search query for environment name.' argument :states, [GraphQL::STRING_TYPE], required: false, - description: 'States of environments that should be included in result' + description: 'States of environments that should be included in result.' type Types::EnvironmentType, null: true diff --git a/app/graphql/resolvers/error_tracking/sentry_detailed_error_resolver.rb b/app/graphql/resolvers/error_tracking/sentry_detailed_error_resolver.rb index 09e76dba645..7032af46221 100644 --- a/app/graphql/resolvers/error_tracking/sentry_detailed_error_resolver.rb +++ b/app/graphql/resolvers/error_tracking/sentry_detailed_error_resolver.rb @@ -7,7 +7,7 @@ module Resolvers argument :id, ::Types::GlobalIDType[::Gitlab::ErrorTracking::DetailedError], required: true, - description: 'ID of the Sentry issue' + description: 'ID of the Sentry issue.' def resolve(id:) # TODO: remove this line when the compatibility layer is removed diff --git a/app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb b/app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb index 13b5672d750..8876f8badcd 100644 --- a/app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb +++ b/app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb @@ -7,7 +7,7 @@ module Resolvers argument :id, ::Types::GlobalIDType[::Gitlab::ErrorTracking::DetailedError], required: true, - description: 'ID of the Sentry issue' + description: 'ID of the Sentry issue.' def resolve(id:) # TODO: remove this line when the compatibility layer is removed diff --git a/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb b/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb index e844ffedbeb..4cd65daa655 100644 --- a/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb +++ b/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb @@ -7,12 +7,12 @@ module Resolvers extension Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension argument :search_term, ::GraphQL::STRING_TYPE, - description: 'Search query for the Sentry error details', + description: 'Search query for the Sentry error details.', required: false # TODO: convert to Enum argument :sort, ::GraphQL::STRING_TYPE, - description: 'Attribute to sort on. Options are frequency, first_seen, last_seen. last_seen is default', + description: 'Attribute to sort on. Options are frequency, first_seen, last_seen. last_seen is default.', required: false delegate :project, to: :object diff --git a/app/graphql/resolvers/full_path_resolver.rb b/app/graphql/resolvers/full_path_resolver.rb index cbb0bf998a6..d01cdf749a1 100644 --- a/app/graphql/resolvers/full_path_resolver.rb +++ b/app/graphql/resolvers/full_path_resolver.rb @@ -7,7 +7,7 @@ module Resolvers prepended do argument :full_path, GraphQL::ID_TYPE, required: true, - description: 'The full path of the project, group or namespace, e.g., "gitlab-org/gitlab-foss"' + description: 'The full path of the project, group or namespace, e.g., "gitlab-org/gitlab-foss".' end def model_by_full_path(model, full_path) diff --git a/app/graphql/resolvers/group_members_resolver.rb b/app/graphql/resolvers/group_members_resolver.rb index fcdf7c01d8b..36e1977b756 100644 --- a/app/graphql/resolvers/group_members_resolver.rb +++ b/app/graphql/resolvers/group_members_resolver.rb @@ -7,7 +7,7 @@ module Resolvers authorize :read_group_member argument :relations, [Types::GroupMemberRelationEnum], - description: 'Filter members by the given member relations', + description: 'Filter members by the given member relations.', required: false, default_value: GroupMembersFinder::DEFAULT_RELATIONS diff --git a/app/graphql/resolvers/group_milestones_resolver.rb b/app/graphql/resolvers/group_milestones_resolver.rb index 83b82e2720b..179283fd7b7 100644 --- a/app/graphql/resolvers/group_milestones_resolver.rb +++ b/app/graphql/resolvers/group_milestones_resolver.rb @@ -5,7 +5,7 @@ module Resolvers class GroupMilestonesResolver < MilestonesResolver argument :include_descendants, GraphQL::BOOLEAN_TYPE, required: false, - description: 'Also return milestones in all subgroups and subprojects' + description: 'Also return milestones in all subgroups and subprojects.' type Types::MilestoneType.connection_type, null: true diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb index ae27cce9113..ac3bdda0f12 100644 --- a/app/graphql/resolvers/issues_resolver.rb +++ b/app/graphql/resolvers/issues_resolver.rb @@ -6,9 +6,9 @@ module Resolvers argument :state, Types::IssuableStateEnum, required: false, - description: 'Current state of this issue' + description: 'Current state of this issue.' argument :sort, Types::IssueSortEnum, - description: 'Sort issues by this criteria', + description: 'Sort issues by this criteria.', required: false, default_value: :created_desc @@ -19,7 +19,7 @@ module Resolvers milestone_due_asc milestone_due_desc].freeze def continue_issue_resolve(parent, finder, **args) - issues = apply_lookahead(Gitlab::Graphql::Loaders::IssuableLoader.new(parent, finder).batching_find_all) + issues = Gitlab::Graphql::Loaders::IssuableLoader.new(parent, finder).batching_find_all { |q| apply_lookahead(q) } if non_stable_cursor_sort?(args[:sort]) # Certain complex sorts are not supported by the stable cursor pagination yet. @@ -32,6 +32,14 @@ module Resolvers private + def unconditional_includes + [ + { + project: [:project_feature] + } + ] + end + def preloads { alert_management_alert: [:alert_management_alert], diff --git a/app/graphql/resolvers/members_resolver.rb b/app/graphql/resolvers/members_resolver.rb index cf51fd298bd..76c3ae936ee 100644 --- a/app/graphql/resolvers/members_resolver.rb +++ b/app/graphql/resolvers/members_resolver.rb @@ -9,7 +9,7 @@ module Resolvers argument :search, GraphQL::STRING_TYPE, required: false, - description: 'Search query' + description: 'Search query.' def resolve_with_lookahead(**args) authorize!(object) diff --git a/app/graphql/resolvers/merge_request_resolver.rb b/app/graphql/resolvers/merge_request_resolver.rb index 4cad65fa697..8fd33c6626e 100644 --- a/app/graphql/resolvers/merge_request_resolver.rb +++ b/app/graphql/resolvers/merge_request_resolver.rb @@ -11,7 +11,7 @@ module Resolvers argument :iid, GraphQL::STRING_TYPE, required: true, as: :iids, - description: 'IID of the merge request, for example `1`' + description: 'IID of the merge request, for example `1`.' def no_results_possible?(args) project.nil? diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb index 98c95565778..ecbdaaa3f55 100644 --- a/app/graphql/resolvers/merge_requests_resolver.rb +++ b/app/graphql/resolvers/merge_requests_resolver.rb @@ -11,24 +11,24 @@ module Resolvers def self.accept_assignee argument :assignee_username, GraphQL::STRING_TYPE, required: false, - description: 'Username of the assignee' + description: 'Username of the assignee.' end def self.accept_author argument :author_username, GraphQL::STRING_TYPE, required: false, - description: 'Username of the author' + description: 'Username of the author.' end def self.accept_reviewer argument :reviewer_username, GraphQL::STRING_TYPE, required: false, - description: 'Username of the reviewer' + description: 'Username of the reviewer.' end argument :iids, [GraphQL::STRING_TYPE], required: false, - description: 'Array of IIDs of merge requests, for example `[1, 2]`' + description: 'Array of IIDs of merge requests, for example `[1, 2]`.' argument :source_branches, [GraphQL::STRING_TYPE], required: false, @@ -50,15 +50,15 @@ module Resolvers description: 'Array of label names. All resolved merge requests will have all of these labels.' argument :merged_after, Types::TimeType, required: false, - description: 'Merge requests merged after this date' + description: 'Merge requests merged after this date.' argument :merged_before, Types::TimeType, required: false, - description: 'Merge requests merged before this date' + description: 'Merge requests merged before this date.' argument :milestone_title, GraphQL::STRING_TYPE, required: false, - description: 'Title of the milestone' + description: 'Title of the milestone.' argument :sort, Types::MergeRequestSortEnum, - description: 'Sort merge requests by this criteria', + description: 'Sort merge requests by this criteria.', required: false, default_value: :created_desc diff --git a/app/graphql/resolvers/metrics/dashboard_resolver.rb b/app/graphql/resolvers/metrics/dashboard_resolver.rb index 18a654c7dc5..f569cb0b2c3 100644 --- a/app/graphql/resolvers/metrics/dashboard_resolver.rb +++ b/app/graphql/resolvers/metrics/dashboard_resolver.rb @@ -5,7 +5,7 @@ module Resolvers class DashboardResolver < Resolvers::BaseResolver argument :path, GraphQL::STRING_TYPE, required: true, - description: "Path to a file which defines metrics dashboard eg: 'config/prometheus/common_metrics.yml'" + description: "Path to a file which defines metrics dashboard eg: 'config/prometheus/common_metrics.yml'." type Types::Metrics::DashboardType, null: true diff --git a/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb b/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb index 2dd224bb17b..9d6b0486c04 100644 --- a/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb +++ b/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb @@ -6,11 +6,11 @@ module Resolvers class AnnotationResolver < Resolvers::BaseResolver argument :from, Types::TimeType, required: true, - description: "Timestamp marking date and time from which annotations need to be fetched" + description: "Timestamp marking date and time from which annotations need to be fetched." argument :to, Types::TimeType, required: false, - description: "Timestamp marking date and time to which annotations need to be fetched" + description: "Timestamp marking date and time to which annotations need to be fetched." type Types::Metrics::Dashboards::AnnotationType, null: true diff --git a/app/graphql/resolvers/milestones_resolver.rb b/app/graphql/resolvers/milestones_resolver.rb index 564e388d571..9a715e4d08b 100644 --- a/app/graphql/resolvers/milestones_resolver.rb +++ b/app/graphql/resolvers/milestones_resolver.rb @@ -7,23 +7,23 @@ module Resolvers argument :ids, [GraphQL::ID_TYPE], required: false, - description: 'Array of global milestone IDs, e.g., "gid://gitlab/Milestone/1"' + description: 'Array of global milestone IDs, e.g., "gid://gitlab/Milestone/1".' argument :state, Types::MilestoneStateEnum, required: false, - description: 'Filter milestones by state' + description: 'Filter milestones by state.' argument :title, GraphQL::STRING_TYPE, required: false, - description: 'The title of the milestone' + description: 'The title of the milestone.' argument :search_title, GraphQL::STRING_TYPE, required: false, - description: 'A search string for the title' + description: 'A search string for the title.' argument :containing_date, Types::TimeType, required: false, - description: 'A date that the milestone contains' + description: 'A date that the milestone contains.' type Types::MilestoneType.connection_type, null: true diff --git a/app/graphql/resolvers/namespace_projects_resolver.rb b/app/graphql/resolvers/namespace_projects_resolver.rb index 9f57c8f3405..da44b9b5623 100644 --- a/app/graphql/resolvers/namespace_projects_resolver.rb +++ b/app/graphql/resolvers/namespace_projects_resolver.rb @@ -5,17 +5,17 @@ module Resolvers argument :include_subgroups, GraphQL::BOOLEAN_TYPE, required: false, default_value: false, - description: 'Include also subgroup projects' + description: 'Include also subgroup projects.' argument :search, GraphQL::STRING_TYPE, required: false, default_value: nil, - description: 'Search project with most similar names or paths' + description: 'Search project with most similar names or paths.' argument :sort, Types::Projects::NamespaceProjectSortEnum, required: false, default_value: nil, - description: 'Sort projects by this criteria' + description: 'Sort projects by this criteria.' type Types::ProjectType, null: true diff --git a/app/graphql/resolvers/package_details_resolver.rb b/app/graphql/resolvers/package_details_resolver.rb new file mode 100644 index 00000000000..dcf4430e55f --- /dev/null +++ b/app/graphql/resolvers/package_details_resolver.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Resolvers + # No return types defined because they can be different. + # rubocop: disable Graphql/ResolverType + class PackageDetailsResolver < BaseResolver + argument :id, ::Types::GlobalIDType[::Packages::Package], + required: true, + description: 'The global ID of the package.' + + def resolve(id:) + # TODO: remove this line when the compatibility layer is removed + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883 + id = ::Types::GlobalIDType[::Packages::Package].coerce_isolated_input(id) + GitlabSchema.find_by_gid(id) + end + end +end diff --git a/app/graphql/resolvers/packages_resolver.rb b/app/graphql/resolvers/packages_resolver.rb index 519fb87183e..d19099e94d4 100644 --- a/app/graphql/resolvers/packages_resolver.rb +++ b/app/graphql/resolvers/packages_resolver.rb @@ -2,7 +2,7 @@ module Resolvers class PackagesResolver < BaseResolver - type Types::PackageType, null: true + type Types::Packages::PackageType, null: true def resolve(**args) return unless packages_available? diff --git a/app/graphql/resolvers/project_members_resolver.rb b/app/graphql/resolvers/project_members_resolver.rb index 659b12c2563..e889b47c000 100644 --- a/app/graphql/resolvers/project_members_resolver.rb +++ b/app/graphql/resolvers/project_members_resolver.rb @@ -6,7 +6,7 @@ module Resolvers authorize :read_project_member argument :relations, [Types::ProjectMemberRelationEnum], - description: 'Filter members by the given member relations', + description: 'Filter members by the given member relations.', required: false, default_value: MembersFinder::DEFAULT_RELATIONS diff --git a/app/graphql/resolvers/project_milestones_resolver.rb b/app/graphql/resolvers/project_milestones_resolver.rb index c88c9ce7219..4cd9cb53f56 100644 --- a/app/graphql/resolvers/project_milestones_resolver.rb +++ b/app/graphql/resolvers/project_milestones_resolver.rb @@ -5,7 +5,7 @@ module Resolvers class ProjectMilestonesResolver < MilestonesResolver argument :include_ancestors, GraphQL::BOOLEAN_TYPE, required: false, - description: "Also return milestones in the project's parent group and its ancestors" + description: "Also return milestones in the project's parent group and its ancestors." type Types::MilestoneType.connection_type, null: true diff --git a/app/graphql/resolvers/project_pipeline_resolver.rb b/app/graphql/resolvers/project_pipeline_resolver.rb index 8bf4e0b08ef..b604a408928 100644 --- a/app/graphql/resolvers/project_pipeline_resolver.rb +++ b/app/graphql/resolvers/project_pipeline_resolver.rb @@ -8,7 +8,7 @@ module Resolvers argument :iid, GraphQL::ID_TYPE, required: true, - description: 'IID of the Pipeline, e.g., "1"' + description: 'IID of the Pipeline, e.g., "1".' def resolve(iid:) BatchLoader::GraphQL.for(iid).batch(key: project) do |iids, loader, args| diff --git a/app/graphql/resolvers/projects/jira_projects_resolver.rb b/app/graphql/resolvers/projects/jira_projects_resolver.rb index 31f42d305b0..de85e8c42e6 100644 --- a/app/graphql/resolvers/projects/jira_projects_resolver.rb +++ b/app/graphql/resolvers/projects/jira_projects_resolver.rb @@ -6,11 +6,12 @@ module Resolvers include Gitlab::Graphql::Authorize::AuthorizeResource type Types::Projects::Services::JiraProjectType.connection_type, null: true + authorize :admin_project argument :name, GraphQL::STRING_TYPE, required: false, - description: 'Project name or key' + description: 'Project name or key.' def resolve(name: nil, **args) authorize!(project) @@ -31,10 +32,6 @@ module Resolvers end end - def authorized_resource?(project) - Ability.allowed?(context[:current_user], :admin_project, project) - end - private alias_method :jira_service, :object diff --git a/app/graphql/resolvers/projects/services_resolver.rb b/app/graphql/resolvers/projects/services_resolver.rb index 4f5a6cddbb3..f618bf2df77 100644 --- a/app/graphql/resolvers/projects/services_resolver.rb +++ b/app/graphql/resolvers/projects/services_resolver.rb @@ -12,11 +12,11 @@ module Resolvers argument :active, GraphQL::BOOLEAN_TYPE, required: false, - description: 'Indicates if the service is active' + description: 'Indicates if the service is active.' argument :type, Types::Projects::ServiceTypeEnum, required: false, - description: 'Class name of the service' + description: 'Class name of the service.' alias_method :project, :object diff --git a/app/graphql/resolvers/projects_resolver.rb b/app/graphql/resolvers/projects_resolver.rb index 69438229a50..11d18a0a080 100644 --- a/app/graphql/resolvers/projects_resolver.rb +++ b/app/graphql/resolvers/projects_resolver.rb @@ -6,23 +6,23 @@ module Resolvers argument :membership, GraphQL::BOOLEAN_TYPE, required: false, - description: 'Limit projects that the current user is a member of' + description: 'Limit projects that the current user is a member of.' argument :search, GraphQL::STRING_TYPE, required: false, - description: 'Search query for project name, path, or description' + description: 'Search query for project name, path, or description.' argument :ids, [GraphQL::ID_TYPE], required: false, - description: 'Filter projects by IDs' + description: 'Filter projects by IDs.' argument :search_namespaces, GraphQL::BOOLEAN_TYPE, required: false, - description: 'Include namespace in project search' + description: 'Include namespace in project search.' argument :sort, GraphQL::STRING_TYPE, required: false, - description: 'Sort order of results' + description: 'Sort order of results.' def resolve(**args) ProjectsFinder diff --git a/app/graphql/resolvers/release_milestones_resolver.rb b/app/graphql/resolvers/release_milestones_resolver.rb new file mode 100644 index 00000000000..7582c91272f --- /dev/null +++ b/app/graphql/resolvers/release_milestones_resolver.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Resolvers + class ReleaseMilestonesResolver < BaseResolver + type Types::MilestoneType.connection_type, null: true + + alias_method :release, :object + + def resolve(**args) + offset_pagination(release.milestones.order_by_dates_and_title) + end + end +end diff --git a/app/graphql/resolvers/release_resolver.rb b/app/graphql/resolvers/release_resolver.rb index 1edcc8c70b5..20ef01f8593 100644 --- a/app/graphql/resolvers/release_resolver.rb +++ b/app/graphql/resolvers/release_resolver.rb @@ -6,7 +6,7 @@ module Resolvers argument :tag_name, GraphQL::STRING_TYPE, required: true, - description: 'The name of the tag associated to the release' + description: 'The name of the tag associated to the release.' alias_method :project, :object diff --git a/app/graphql/resolvers/releases_resolver.rb b/app/graphql/resolvers/releases_resolver.rb index 8e8127cf279..01c1e9b11e7 100644 --- a/app/graphql/resolvers/releases_resolver.rb +++ b/app/graphql/resolvers/releases_resolver.rb @@ -6,7 +6,7 @@ module Resolvers argument :sort, Types::ReleaseSortEnum, required: false, default_value: :released_at_desc, - description: 'Sort releases by this criteria' + description: 'Sort releases by this criteria.' alias_method :project, :object diff --git a/app/graphql/resolvers/snippets/blobs_resolver.rb b/app/graphql/resolvers/snippets/blobs_resolver.rb index cfb1711aed4..868d34ae7ad 100644 --- a/app/graphql/resolvers/snippets/blobs_resolver.rb +++ b/app/graphql/resolvers/snippets/blobs_resolver.rb @@ -13,7 +13,7 @@ module Resolvers argument :paths, [GraphQL::STRING_TYPE], required: false, - description: 'Paths of the blobs' + description: 'Paths of the blobs.' def resolve(**args) authorize!(snippet) diff --git a/app/graphql/resolvers/snippets_resolver.rb b/app/graphql/resolvers/snippets_resolver.rb index 77099565df0..7153c919062 100644 --- a/app/graphql/resolvers/snippets_resolver.rb +++ b/app/graphql/resolvers/snippets_resolver.rb @@ -11,20 +11,20 @@ module Resolvers argument :author_id, ::Types::GlobalIDType[::User], required: false, - description: 'The ID of an author' + description: 'The ID of an author.' argument :project_id, ::Types::GlobalIDType[::Project], required: false, - description: 'The ID of a project' + description: 'The ID of a project.' argument :type, Types::Snippets::TypeEnum, required: false, - description: 'The type of snippet' + description: 'The type of snippet.' argument :explore, GraphQL::BOOLEAN_TYPE, required: false, - description: 'Explore personal snippets' + description: 'Explore personal snippets.' def resolve(**args) if args[:author_id].present? && args[:project_id].present? diff --git a/app/graphql/resolvers/todo_resolver.rb b/app/graphql/resolvers/todo_resolver.rb index 9a8f7a71154..8966285fccc 100644 --- a/app/graphql/resolvers/todo_resolver.rb +++ b/app/graphql/resolvers/todo_resolver.rb @@ -8,27 +8,27 @@ module Resolvers argument :action, [Types::TodoActionEnum], required: false, - description: 'The action to be filtered' + description: 'The action to be filtered.' argument :author_id, [GraphQL::ID_TYPE], required: false, - description: 'The ID of an author' + description: 'The ID of an author.' argument :project_id, [GraphQL::ID_TYPE], required: false, - description: 'The ID of a project' + description: 'The ID of a project.' argument :group_id, [GraphQL::ID_TYPE], required: false, - description: 'The ID of a group' + description: 'The ID of a group.' argument :state, [Types::TodoStateEnum], required: false, - description: 'The state of the todo' + description: 'The state of the todo.' argument :type, [Types::TodoTargetEnum], required: false, - description: 'The type of the todo' + description: 'The type of the todo.' def resolve(**args) return Todo.none unless current_user.present? && target.present? diff --git a/app/graphql/resolvers/tree_resolver.rb b/app/graphql/resolvers/tree_resolver.rb index 075a1929c47..7a70c35897d 100644 --- a/app/graphql/resolvers/tree_resolver.rb +++ b/app/graphql/resolvers/tree_resolver.rb @@ -7,15 +7,15 @@ module Resolvers argument :path, GraphQL::STRING_TYPE, required: false, default_value: '', - description: 'The path to get the tree for. Default value is the root of the repository' + description: 'The path to get the tree for. Default value is the root of the repository.' argument :ref, GraphQL::STRING_TYPE, required: false, default_value: :head, - description: 'The commit ref to get the tree for. Default value is HEAD' + description: 'The commit ref to get the tree for. Default value is HEAD.' argument :recursive, GraphQL::BOOLEAN_TYPE, required: false, default_value: false, - description: 'Used to get a recursive tree. Default is false' + description: 'Used to get a recursive tree. Default is false.' alias_method :repository, :object diff --git a/app/graphql/resolvers/user_resolver.rb b/app/graphql/resolvers/user_resolver.rb index 06c1f0cb42d..84bc03091d9 100644 --- a/app/graphql/resolvers/user_resolver.rb +++ b/app/graphql/resolvers/user_resolver.rb @@ -8,11 +8,11 @@ module Resolvers argument :id, Types::GlobalIDType[User], required: false, - description: 'ID of the User' + description: 'ID of the User.' argument :username, GraphQL::STRING_TYPE, required: false, - description: 'Username of the User' + description: 'Username of the User.' def ready?(id: nil, username: nil) unless id.present? ^ username.present? diff --git a/app/graphql/resolvers/user_starred_projects_resolver.rb b/app/graphql/resolvers/user_starred_projects_resolver.rb index cc3bb90decf..db420b3d116 100644 --- a/app/graphql/resolvers/user_starred_projects_resolver.rb +++ b/app/graphql/resolvers/user_starred_projects_resolver.rb @@ -6,7 +6,7 @@ module Resolvers argument :search, GraphQL::STRING_TYPE, required: false, - description: 'Search query' + description: 'Search query.' alias_method :user, :object diff --git a/app/graphql/resolvers/users/snippets_resolver.rb b/app/graphql/resolvers/users/snippets_resolver.rb index c2d42437ffd..e8048b9deb9 100644 --- a/app/graphql/resolvers/users/snippets_resolver.rb +++ b/app/graphql/resolvers/users/snippets_resolver.rb @@ -10,7 +10,7 @@ module Resolvers argument :type, Types::Snippets::TypeEnum, required: false, - description: 'The type of snippet' + description: 'The type of snippet.' private diff --git a/app/graphql/resolvers/users_resolver.rb b/app/graphql/resolvers/users_resolver.rb index a0ed076595d..95ced131504 100644 --- a/app/graphql/resolvers/users_resolver.rb +++ b/app/graphql/resolvers/users_resolver.rb @@ -9,13 +9,13 @@ module Resolvers argument :ids, [GraphQL::ID_TYPE], required: false, - description: 'List of user Global IDs' + description: 'List of user Global IDs.' argument :usernames, [GraphQL::STRING_TYPE], required: false, - description: 'List of usernames' + description: 'List of usernames.' argument :sort, Types::SortEnum, - description: 'Sort users by this criteria', + description: 'Sort users by this criteria.', required: false, default_value: :created_desc @@ -23,10 +23,15 @@ module Resolvers required: false, description: "Query to search users by name, username, or primary email." - def resolve(ids: nil, usernames: nil, sort: nil, search: nil) + argument :admins, GraphQL::BOOLEAN_TYPE, + required: false, + default_value: false, + description: 'Return only admin users.' + + def resolve(ids: nil, usernames: nil, sort: nil, search: nil, admins: nil) authorize! - ::UsersFinder.new(context[:current_user], finder_params(ids, usernames, sort, search)).execute + ::UsersFinder.new(context[:current_user], finder_params(ids, usernames, sort, search, admins)).execute end def ready?(**args) @@ -34,7 +39,7 @@ module Resolvers return super if args.values.compact.blank? - if args.values.all? + if args[:usernames]&.present? && args[:ids]&.present? raise Gitlab::Graphql::Errors::ArgumentError, 'Provide either a list of usernames or ids' end @@ -47,12 +52,13 @@ module Resolvers private - def finder_params(ids, usernames, sort, search) + def finder_params(ids, usernames, sort, search, admins) params = {} params[:sort] = sort if sort params[:username] = usernames if usernames params[:id] = parse_gids(ids) if ids params[:search] = search if search + params[:admins] = admins if admins params end diff --git a/app/graphql/types/admin/analytics/instance_statistics/measurement_type.rb b/app/graphql/types/admin/analytics/instance_statistics/measurement_type.rb index d45341077a4..eab42c2b78d 100644 --- a/app/graphql/types/admin/analytics/instance_statistics/measurement_type.rb +++ b/app/graphql/types/admin/analytics/instance_statistics/measurement_type.rb @@ -1,14 +1,16 @@ # frozen_string_literal: true -# rubocop:disable Graphql/AuthorizeTypes module Types module Admin module Analytics module InstanceStatistics class MeasurementType < BaseObject + include Gitlab::Graphql::Authorize::AuthorizeResource graphql_name 'InstanceStatisticsMeasurement' description 'Represents a recorded measurement (object count) for the Admins' + authorize :read_instance_statistics_measurements + field :recorded_at, Types::TimeType, null: true, description: 'The time the measurement was recorded' diff --git a/app/graphql/types/alert_management/domain_filter_enum.rb b/app/graphql/types/alert_management/domain_filter_enum.rb index 58dbc8bb2cf..a798cfb9ee9 100644 --- a/app/graphql/types/alert_management/domain_filter_enum.rb +++ b/app/graphql/types/alert_management/domain_filter_enum.rb @@ -6,7 +6,7 @@ module Types graphql_name 'AlertManagementDomainFilter' description 'Filters the alerts based on given domain' - value 'operations', description: 'Alerts for operations domain ' + value 'operations', description: 'Alerts for operations domain' value 'threat_monitoring', description: 'Alerts for threat monitoring domain' end end diff --git a/app/graphql/types/base_enum.rb b/app/graphql/types/base_enum.rb index 159443641bc..cbd45b46dd6 100644 --- a/app/graphql/types/base_enum.rb +++ b/app/graphql/types/base_enum.rb @@ -5,6 +5,25 @@ module Types extend GitlabStyleDeprecations class << self + # Registers enum definition by the given DeclarativeEnum module + # + # @param enum_mod [Module] The enum module to be used + # @param use_name [Boolean] Does not override the name if set `false` + # @param use_description [Boolean] Does not override the description if set `false` + # + # Example: + # + # class MyEnum < BaseEnum + # declarative_enum MyDeclarativeEnum + # end + # + def declarative_enum(enum_mod, use_name: true, use_description: true) + graphql_name(enum_mod.name) if use_name + description(enum_mod.description) if use_description + + enum_mod.definition.each { |key, content| value(key.to_s.upcase, content) } + end + def value(*args, **kwargs, &block) enum[args[0].downcase] = kwargs[:value] || args[0] kwargs = gitlab_deprecation(kwargs) diff --git a/app/graphql/types/board_type.rb b/app/graphql/types/board_type.rb index f47c744d1bb..f576fd83840 100644 --- a/app/graphql/types/board_type.rb +++ b/app/graphql/types/board_type.rb @@ -7,6 +7,8 @@ module Types accepts ::Board authorize :read_board + present_using BoardPresenter + field :id, type: GraphQL::ID_TYPE, null: false, description: 'ID (global ID) of the board' field :name, type: GraphQL::STRING_TYPE, null: true, @@ -24,6 +26,12 @@ module Types description: 'Lists of the board', resolver: Resolvers::BoardListsResolver, extras: [:lookahead] + + field :web_path, GraphQL::STRING_TYPE, null: false, + description: 'Web path of the board.' + + field :web_url, GraphQL::STRING_TYPE, null: false, + description: 'Web URL of the board.' end end diff --git a/app/graphql/types/ci/build_need_type.rb b/app/graphql/types/ci/build_need_type.rb new file mode 100644 index 00000000000..3bd81f8fa8f --- /dev/null +++ b/app/graphql/types/ci/build_need_type.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + # This type is only accessible from CiJob + class BuildNeedType < BaseObject + graphql_name 'CiBuildNeed' + + field :name, GraphQL::STRING_TYPE, null: true, + description: 'Name of the job we need to complete.' + end + end +end diff --git a/app/graphql/types/ci/ci_cd_setting_type.rb b/app/graphql/types/ci/ci_cd_setting_type.rb index 207c37f9538..e80771cdf9d 100644 --- a/app/graphql/types/ci/ci_cd_setting_type.rb +++ b/app/graphql/types/ci/ci_cd_setting_type.rb @@ -11,8 +11,10 @@ module Types description: 'Whether merge pipelines are enabled.', method: :merge_pipelines_enabled? field :merge_trains_enabled, GraphQL::BOOLEAN_TYPE, null: true, - description: 'Whether merge trains are enabled.', + description: 'Whether merge trains are enabled.', method: :merge_trains_enabled? + field :keep_latest_artifact, GraphQL::BOOLEAN_TYPE, null: true, + description: 'Whether to keep the latest builds artifacts.' field :project, Types::ProjectType, null: true, description: 'Project the CI/CD settings belong to.' end diff --git a/app/graphql/types/ci/config/config_type.rb b/app/graphql/types/ci/config/config_type.rb index e54b345f3d3..29093c6d3c9 100644 --- a/app/graphql/types/ci/config/config_type.rb +++ b/app/graphql/types/ci/config/config_type.rb @@ -11,7 +11,7 @@ module Types description: 'Linting errors' field :merged_yaml, GraphQL::STRING_TYPE, null: true, description: 'Merged CI config YAML' - field :stages, [Types::Ci::Config::StageType], null: true, + field :stages, Types::Ci::Config::StageType.connection_type, null: true, description: 'Stages of the pipeline' field :status, Types::Ci::Config::StatusEnum, null: true, description: 'Status of linting, can be either valid or invalid' diff --git a/app/graphql/types/ci/config/group_type.rb b/app/graphql/types/ci/config/group_type.rb index 8b0db2934a4..8e133bbcba8 100644 --- a/app/graphql/types/ci/config/group_type.rb +++ b/app/graphql/types/ci/config/group_type.rb @@ -9,7 +9,7 @@ module Types field :name, GraphQL::STRING_TYPE, null: true, description: 'Name of the job group' - field :jobs, [Types::Ci::Config::JobType], null: true, + field :jobs, Types::Ci::Config::JobType.connection_type, null: true, description: 'Jobs in group' field :size, GraphQL::INT_TYPE, null: true, description: 'Size of the job group' diff --git a/app/graphql/types/ci/config/job_restriction_type.rb b/app/graphql/types/ci/config/job_restriction_type.rb new file mode 100644 index 00000000000..294e3c94571 --- /dev/null +++ b/app/graphql/types/ci/config/job_restriction_type.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Types + module Ci + # rubocop: disable Graphql/AuthorizeTypes + module Config + class JobRestrictionType < BaseObject + graphql_name 'CiConfigJobRestriction' + + field :refs, [GraphQL::STRING_TYPE], null: true, + description: 'The Git refs the job restriction applies to.' + end + end + end +end diff --git a/app/graphql/types/ci/config/job_type.rb b/app/graphql/types/ci/config/job_type.rb index 59bcbd9ef49..65fdc4c2615 100644 --- a/app/graphql/types/ci/config/job_type.rb +++ b/app/graphql/types/ci/config/job_type.rb @@ -8,13 +8,36 @@ module Types graphql_name 'CiConfigJob' field :name, GraphQL::STRING_TYPE, null: true, - description: 'Name of the job' + description: 'Name of the job.' field :group_name, GraphQL::STRING_TYPE, null: true, - description: 'Name of the job group' + description: 'Name of the job group.' field :stage, GraphQL::STRING_TYPE, null: true, - description: 'Name of the job stage' - field :needs, [Types::Ci::Config::NeedType], null: true, - description: 'Builds that must complete before the jobs run' + description: 'Name of the job stage.' + field :needs, Types::Ci::Config::NeedType.connection_type, null: true, + description: 'Builds that must complete before the jobs run.' + field :allow_failure, GraphQL::BOOLEAN_TYPE, null: true, + description: 'Allow job to fail.' + field :before_script, [GraphQL::STRING_TYPE], null: true, + description: 'Override a set of commands that are executed before the job.' + field :script, [GraphQL::STRING_TYPE], null: true, + description: 'Shell script that is executed by a runner.' + field :after_script, [GraphQL::STRING_TYPE], null: true, + description: 'Override a set of commands that are executed after the job.' + field :when, GraphQL::STRING_TYPE, null: true, + description: 'When to run the job.', + resolver_method: :restrict_when_to_run_jobs + field :environment, GraphQL::STRING_TYPE, null: true, + description: 'Name of an environment to which the job deploys.' + field :except, Types::Ci::Config::JobRestrictionType, null: true, + description: 'Limit when jobs are not created.' + field :only, Types::Ci::Config::JobRestrictionType, null: true, + description: 'Jobs are created when these conditions do not apply.' + field :tags, [GraphQL::STRING_TYPE], null: true, + description: 'List of tags that are used to select a runner.' + + def restrict_when_to_run_jobs + object[:when] + end end end end diff --git a/app/graphql/types/ci/config/stage_type.rb b/app/graphql/types/ci/config/stage_type.rb index 20618bc41f8..2008c553629 100644 --- a/app/graphql/types/ci/config/stage_type.rb +++ b/app/graphql/types/ci/config/stage_type.rb @@ -9,7 +9,7 @@ module Types field :name, GraphQL::STRING_TYPE, null: true, description: 'Name of the stage' - field :groups, [Types::Ci::Config::GroupType], null: true, + field :groups, Types::Ci::Config::GroupType.connection_type, null: true, description: 'Groups of jobs for the stage' end end diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb index 5b6e8fe8567..f8bf1732e63 100644 --- a/app/graphql/types/ci/job_type.rb +++ b/app/graphql/types/ci/job_type.rb @@ -2,16 +2,16 @@ module Types module Ci - # rubocop: disable Graphql/AuthorizeTypes class JobType < BaseObject graphql_name 'CiJob' + authorize :read_commit_status field :pipeline, Types::Ci::PipelineType, null: true, description: 'Pipeline the job belongs to' field :name, GraphQL::STRING_TYPE, null: true, description: 'Name of the job' - field :needs, JobType.connection_type, null: true, - description: 'Builds that must complete before the jobs run' + field :needs, BuildNeedType.connection_type, null: true, + description: 'References to builds that must complete before the jobs run' field :detailed_status, Types::Ci::DetailedStatusType, null: true, description: 'Detailed status of the job' field :scheduled_at, Types::TimeType, null: true, diff --git a/app/graphql/types/ci/stage_type.rb b/app/graphql/types/ci/stage_type.rb index fd0bde90836..695e7c61bd9 100644 --- a/app/graphql/types/ci/stage_type.rb +++ b/app/graphql/types/ci/stage_type.rb @@ -9,6 +9,7 @@ module Types field :name, GraphQL::STRING_TYPE, null: true, description: 'Name of the stage' field :groups, Ci::GroupType.connection_type, null: true, + extras: [:lookahead], description: 'Group of jobs for the stage' field :detailed_status, Types::Ci::DetailedStatusType, null: true, description: 'Detailed status of the stage' @@ -16,6 +17,38 @@ module Types def detailed_status object.detailed_status(context[:current_user]) end + + # Issues one query per pipeline + def groups(lookahead:) + key = ::Gitlab::Graphql::BatchKey.new(object, lookahead, object_name: :stage) + + BatchLoader::GraphQL.for(key).batch(default_value: []) do |keys, loader| + by_pipeline = keys.group_by(&:pipeline) + include_needs = keys.any? { |k| k.requires?(%i[nodes jobs nodes needs]) } + + by_pipeline.each do |pl, key_group| + project = pl.project + indexed = key_group.index_by(&:id) + + jobs_for_pipeline(pl, indexed.keys, include_needs).each do |stage_id, statuses| + key = indexed[stage_id] + groups = ::Ci::Group.fabricate(project, key.stage, statuses) + loader.call(key, groups) + end + end + end + end + + private + + # rubocop: disable CodeReuse/ActiveRecord + def jobs_for_pipeline(pipeline, stage_ids, include_needs) + results = pipeline.latest_statuses.where(stage_id: stage_ids) + results = results.preload(:needs) if include_needs + + results.group_by(&:stage_id) + end + # rubocop: enable CodeReuse/ActiveRecord end end end diff --git a/app/graphql/types/data_visualization_palette/color_enum.rb b/app/graphql/types/data_visualization_palette/color_enum.rb new file mode 100644 index 00000000000..d55dde44048 --- /dev/null +++ b/app/graphql/types/data_visualization_palette/color_enum.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + module DataVisualizationPalette + class ColorEnum < BaseEnum + graphql_name 'DataVisualizationColorEnum' + description 'Color of the data visualization palette' + + Enums::DataVisualizationPalette.colors.keys.each do |unit| + value unit.upcase, value: unit, description: "#{unit.to_s.titleize} color" + end + end + end +end diff --git a/app/graphql/types/data_visualization_palette/weight_enum.rb b/app/graphql/types/data_visualization_palette/weight_enum.rb new file mode 100644 index 00000000000..b2b12a90ee4 --- /dev/null +++ b/app/graphql/types/data_visualization_palette/weight_enum.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + module DataVisualizationPalette + class WeightEnum < BaseEnum + graphql_name 'DataVisualizationWeightEnum' + description 'Weight of the data visualization palette' + + ::Enums::DataVisualizationPalette.weights.keys.each do |unit| + value "weight_#{unit}".upcase, value: unit, description: "#{unit.to_s.titleize} weight" + end + end + end +end diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index 83b8a834801..78fb20650e9 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -121,6 +121,9 @@ module Types field :moved_to, Types::IssueType, null: true, description: 'Updated Issue after it got moved to another project' + field :create_note_email, GraphQL::STRING_TYPE, null: true, + description: 'User specific email address for the issue' + def author Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find end @@ -140,6 +143,10 @@ module Types def discussion_locked !!object.discussion_locked end + + def create_note_email + object.creatable_note_email_address(context[:current_user]) + end end end diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 816160e58f7..ee7d5780f7a 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -96,6 +96,8 @@ module Types description: 'Default merge commit message of the merge request' field :default_merge_commit_message_with_description, GraphQL::STRING_TYPE, null: true, description: 'Default merge commit message of the merge request with description' + field :default_squash_commit_message, GraphQL::STRING_TYPE, null: true, calls_gitaly: true, + description: 'Default squash commit message of the merge request' field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false, description: 'Indicates if a merge is currently occurring' field :source_branch_exists, GraphQL::BOOLEAN_TYPE, @@ -126,10 +128,12 @@ module Types description: 'The milestone of the merge request' field :assignees, Types::UserType.connection_type, null: true, complexity: 5, description: 'Assignees of the merge request' + field :reviewers, Types::UserType.connection_type, null: true, complexity: 5, + description: 'Users from whom a review has been requested.' field :author, Types::UserType, null: true, description: 'User who created this merge request' - field :participants, Types::UserType.connection_type, null: true, complexity: 5, - description: 'Participants in the merge request' + field :participants, Types::UserType.connection_type, null: true, complexity: 15, + description: 'Participants in the merge request. This includes the author, assignees, reviewers, and users mentioned in notes.' field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false, complexity: 5, description: 'Indicates if the currently logged in user is subscribed to this merge request' field :labels, Types::LabelType.connection_type, null: true, complexity: 5, @@ -159,6 +163,8 @@ module Types description: 'Users who approved the merge request' field :squash_on_merge, GraphQL::BOOLEAN_TYPE, null: false, method: :squash_on_merge?, description: 'Indicates if squash on merge is enabled' + field :squash, GraphQL::BOOLEAN_TYPE, null: false, + description: 'Indicates if squash on merge is enabled' field :available_auto_merge_strategies, [GraphQL::STRING_TYPE], null: true, calls_gitaly: true, description: 'Array of available auto merge strategies' field :has_ci, GraphQL::BOOLEAN_TYPE, null: false, method: :has_ci?, @@ -169,6 +175,10 @@ module Types calls_gitaly: true, description: 'Merge request commits excluding merge commits' field :security_auto_fix, GraphQL::BOOLEAN_TYPE, null: true, description: 'Indicates if the merge request is created by @GitLab-Security-Bot.' + field :auto_merge_strategy, GraphQL::STRING_TYPE, null: true, + description: 'Selected auto merge strategy' + field :merge_user, Types::UserType, null: true, + description: 'User who merged this merge request' def approved_by object.approved_by_users @@ -235,6 +245,10 @@ module Types def security_auto_fix object.author == User.security_bot end + + def reviewers + object.reviewers if object.allows_reviewers? + end end end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 9eea81c9d3e..f9dd11cbe37 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -88,9 +88,11 @@ module Types mount_mutation Mutations::ContainerExpirationPolicies::Update mount_mutation Mutations::ContainerRepositories::Destroy mount_mutation Mutations::ContainerRepositories::DestroyTags - mount_mutation Mutations::Ci::PipelineCancel - mount_mutation Mutations::Ci::PipelineDestroy - mount_mutation Mutations::Ci::PipelineRetry + mount_mutation Mutations::Ci::Pipeline::Cancel + mount_mutation Mutations::Ci::Pipeline::Destroy + mount_mutation Mutations::Ci::Pipeline::Retry + mount_mutation Mutations::Ci::CiCdSettingsUpdate + mount_mutation Mutations::Namespace::PackageSettings::Update end end diff --git a/app/graphql/types/namespace/package_settings_type.rb b/app/graphql/types/namespace/package_settings_type.rb new file mode 100644 index 00000000000..0720a1cfb4b --- /dev/null +++ b/app/graphql/types/namespace/package_settings_type.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + class Namespace::PackageSettingsType < BaseObject + graphql_name 'PackageSettings' + + description 'Namespace-level Package Registry settings' + + authorize :read_package_settings + + field :maven_duplicates_allowed, GraphQL::BOOLEAN_TYPE, null: false, description: 'Indicates whether duplicate Maven packages are allowed for this namespace.' + field :maven_duplicate_exception_regex, Types::UntrustedRegexp, null: true, description: 'When maven_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect.' + end +end diff --git a/app/graphql/types/namespace_type.rb b/app/graphql/types/namespace_type.rb index 4dec6f4c5e6..ab614d92b06 100644 --- a/app/graphql/types/namespace_type.rb +++ b/app/graphql/types/namespace_type.rb @@ -37,6 +37,11 @@ module Types description: 'Projects within this namespace', resolver: ::Resolvers::NamespaceProjectsResolver + field :package_settings, + Types::Namespace::PackageSettingsType, + null: true, + description: 'The package settings for the namespace' + def root_storage_statistics Gitlab::Graphql::Loaders::BatchRootStorageStatisticsLoader.new(object.id).find end diff --git a/app/graphql/types/notes/note_type.rb b/app/graphql/types/notes/note_type.rb index f4e05e19eca..84b61340e93 100644 --- a/app/graphql/types/notes/note_type.rb +++ b/app/graphql/types/notes/note_type.rb @@ -46,6 +46,13 @@ module Types field :confidential, GraphQL::BOOLEAN_TYPE, null: true, description: 'Indicates if this note is confidential', method: :confidential? + field :url, GraphQL::STRING_TYPE, + null: true, + description: 'URL to view this Note in the Web UI' + + def url + ::Gitlab::UrlBuilder.build(object) + end def system_note_icon_name SystemNoteHelper.system_note_icon_name(object) if object.system? diff --git a/app/graphql/types/package_type.rb b/app/graphql/types/package_type.rb deleted file mode 100644 index 0604bf827a5..00000000000 --- a/app/graphql/types/package_type.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -module Types - class PackageType < BaseObject - graphql_name 'Package' - description 'Represents a package' - authorize :read_package - - field :id, GraphQL::ID_TYPE, null: false, description: 'The ID of the package' - field :name, GraphQL::STRING_TYPE, null: false, description: 'The name of the package' - field :created_at, Types::TimeType, null: false, description: 'The created date' - field :updated_at, Types::TimeType, null: false, description: 'The update date' - field :version, GraphQL::STRING_TYPE, null: true, description: 'The version of the package' - field :package_type, Types::PackageTypeEnum, null: false, description: 'The type of the package' - end -end diff --git a/app/graphql/types/package_type_enum.rb b/app/graphql/types/package_type_enum.rb deleted file mode 100644 index 6f50c166da3..00000000000 --- a/app/graphql/types/package_type_enum.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module Types - class PackageTypeEnum < BaseEnum - PACKAGE_TYPE_NAMES = { - pypi: 'PyPI', - npm: 'NPM' - }.freeze - - ::Packages::Package.package_types.keys.each do |package_type| - type_name = PACKAGE_TYPE_NAMES.fetch(package_type.to_sym, package_type.capitalize) - value package_type.to_s.upcase, "Packages from the #{type_name} package manager", value: package_type.to_s - end - end -end diff --git a/app/graphql/types/packages/composer/details_type.rb b/app/graphql/types/packages/composer/details_type.rb new file mode 100644 index 00000000000..8c6845a6fb3 --- /dev/null +++ b/app/graphql/types/packages/composer/details_type.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + module Packages + module Composer + class DetailsType < Types::Packages::PackageType + graphql_name 'PackageComposerDetails' + description 'Details of a Composer package' + + authorize :read_package + + field :composer_metadatum, Types::Packages::Composer::MetadatumType, null: false, description: 'The Composer metadatum.' + end + end + end +end diff --git a/app/graphql/types/packages/composer/json_type.rb b/app/graphql/types/packages/composer/json_type.rb new file mode 100644 index 00000000000..b7aa32f0170 --- /dev/null +++ b/app/graphql/types/packages/composer/json_type.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + module Packages + module Composer + # rubocop: disable Graphql/AuthorizeTypes + class JsonType < BaseObject + graphql_name 'PackageComposerJsonType' + description 'Represents a composer JSON file' + + field :name, GraphQL::STRING_TYPE, null: true, description: 'The name set in the Composer JSON file.' + field :type, GraphQL::STRING_TYPE, null: true, description: 'The type set in the Composer JSON file.' + field :license, GraphQL::STRING_TYPE, null: true, description: 'The license set in the Composer JSON file.' + field :version, GraphQL::STRING_TYPE, null: true, description: 'The version set in the Composer JSON file.' + end + end + end +end diff --git a/app/graphql/types/packages/composer/metadatum_type.rb b/app/graphql/types/packages/composer/metadatum_type.rb new file mode 100644 index 00000000000..a97818b1fb8 --- /dev/null +++ b/app/graphql/types/packages/composer/metadatum_type.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Types + module Packages + module Composer + class MetadatumType < BaseObject + graphql_name 'PackageComposerMetadatumType' + description 'Composer metadatum' + + authorize :read_package + + field :target_sha, GraphQL::STRING_TYPE, null: false, description: 'Target SHA of the package.' + field :composer_json, Types::Packages::Composer::JsonType, null: false, description: 'Data of the Composer JSON file.' + end + end + end +end diff --git a/app/graphql/types/packages/package_tag_type.rb b/app/graphql/types/packages/package_tag_type.rb new file mode 100644 index 00000000000..a05ce03da67 --- /dev/null +++ b/app/graphql/types/packages/package_tag_type.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + module Packages + class PackageTagType < BaseObject + graphql_name 'PackageTag' + description 'Represents a package tag' + authorize :read_package + + field :id, GraphQL::ID_TYPE, null: false, description: 'The ID of the tag.' + field :name, GraphQL::STRING_TYPE, null: false, description: 'The name of the tag.' + field :created_at, Types::TimeType, null: false, description: 'The created date.' + field :updated_at, Types::TimeType, null: false, description: 'The updated date.' + end + end +end diff --git a/app/graphql/types/packages/package_type.rb b/app/graphql/types/packages/package_type.rb new file mode 100644 index 00000000000..b13d16e91c6 --- /dev/null +++ b/app/graphql/types/packages/package_type.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Types + module Packages + class PackageType < BaseObject + graphql_name 'Package' + description 'Represents a package in the Package Registry' + + authorize :read_package + + field :id, GraphQL::ID_TYPE, null: false, description: 'The ID of the package.' + field :name, GraphQL::STRING_TYPE, null: false, description: 'The name of the package.' + field :created_at, Types::TimeType, null: false, description: 'The created date.' + field :updated_at, Types::TimeType, null: false, description: 'The updated date.' + field :version, GraphQL::STRING_TYPE, null: true, description: 'The version of the package.' + field :package_type, Types::Packages::PackageTypeEnum, null: false, description: 'The type of the package.' + field :tags, Types::Packages::PackageTagType.connection_type, null: true, description: 'The package tags.' + field :project, Types::ProjectType, null: false, description: 'Project where the package is stored.' + field :pipelines, Types::Ci::PipelineType.connection_type, null: true, description: 'Pipelines that built the package.' + field :versions, Types::Packages::PackageType.connection_type, null: true, description: 'The other versions of the package.' + + def project + Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find + end + end + end +end diff --git a/app/graphql/types/packages/package_type_enum.rb b/app/graphql/types/packages/package_type_enum.rb new file mode 100644 index 00000000000..9713c9d49b1 --- /dev/null +++ b/app/graphql/types/packages/package_type_enum.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Types + module Packages + class PackageTypeEnum < BaseEnum + PACKAGE_TYPE_NAMES = { + pypi: 'PyPI', + npm: 'NPM' + }.freeze + + ::Packages::Package.package_types.keys.each do |package_type| + type_name = PACKAGE_TYPE_NAMES.fetch(package_type.to_sym, package_type.capitalize) + value package_type.to_s.upcase, "Packages from the #{type_name} package manager", value: package_type.to_s + end + end + end +end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index a7d9548610e..f66d8926a9f 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -175,7 +175,7 @@ module Types description: 'A single issue of the project', resolver: Resolvers::IssuesResolver.single - field :packages, Types::PackageType.connection_type, null: true, + field :packages, Types::Packages::PackageType.connection_type, null: true, description: 'Packages of the project', resolver: Resolvers::PackagesResolver @@ -315,9 +315,6 @@ module Types description: 'Pipeline analytics', resolver: Resolvers::ProjectPipelineStatisticsResolver - field :total_pipeline_duration, GraphQL::INT_TYPE, null: true, - description: 'Total pipeline duration for all of the pipelines in a project' - def label(title:) BatchLoader::GraphQL.for(title).batch(key: project) do |titles, loader, args| LabelsFinder @@ -362,10 +359,6 @@ module Types project.container_repositories.size end - def total_pipeline_duration - object.all_pipelines.total_duration - end - private def project diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 05bb371088c..0e0c060f374 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -58,6 +58,11 @@ module Types argument :id, ::Types::GlobalIDType[::ContainerRepository], required: true, description: 'The global ID of the container repository' end + field :package_composer_details, Types::Packages::Composer::DetailsType, + null: true, + description: 'Find a composer package', + resolver: Resolvers::PackageDetailsResolver + field :user, Types::UserType, null: true, description: 'Find a user', diff --git a/app/graphql/types/release_type.rb b/app/graphql/types/release_type.rb index b715b981483..e7afa7ce7f7 100644 --- a/app/graphql/types/release_type.rb +++ b/app/graphql/types/release_type.rb @@ -35,7 +35,8 @@ module Types field :links, Types::ReleaseLinksType, null: true, method: :itself, description: 'Links of the release' field :milestones, Types::MilestoneType.connection_type, null: true, - description: 'Milestones associated to the release' + description: 'Milestones associated to the release', + resolver: ::Resolvers::ReleaseMilestonesResolver field :evidences, Types::EvidenceType.connection_type, null: true, description: 'Evidence for the release' diff --git a/app/graphql/types/repository_type.rb b/app/graphql/types/repository_type.rb index f0c25e13a26..5b86871142c 100644 --- a/app/graphql/types/repository_type.rb +++ b/app/graphql/types/repository_type.rb @@ -10,7 +10,7 @@ module Types description: 'Default branch of the repository' field :empty, GraphQL::BOOLEAN_TYPE, null: false, method: :empty?, calls_gitaly: true, description: 'Indicates repository has no visible content' - field :exists, GraphQL::BOOLEAN_TYPE, null: false, method: :exists?, + field :exists, GraphQL::BOOLEAN_TYPE, null: false, method: :exists?, calls_gitaly: true, description: 'Indicates a corresponding Git repository exists on disk' field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true, description: 'Tree of the repository' diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 512ba7e2a66..2a1652cf2ba 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -4,8 +4,6 @@ require 'digest/md5' require 'uri' module ApplicationHelper - include StartupCssHelper - # See https://docs.gitlab.com/ee/development/ee_features.html#code-in-appviews # rubocop: disable CodeReuse/ActiveRecord # We allow partial to be nil so that collection views can be passed in @@ -46,7 +44,7 @@ module ApplicationHelper # current_controller?('gitlab/application') # => false def current_controller?(*args) args.any? do |v| - v.to_s.downcase == controller.controller_name || v.to_s.downcase == controller.controller_path + Gitlab::Utils.safe_downcase!(v.to_s) == controller.controller_name || Gitlab::Utils.safe_downcase!(v.to_s) == controller.controller_path end end @@ -61,7 +59,7 @@ module ApplicationHelper # current_action?(:create) # => false # current_action?(:new, :create) # => true def current_action?(*args) - args.any? { |v| v.to_s.downcase == action_name } + args.any? { |v| Gitlab::Utils.safe_downcase!(v.to_s) == action_name } end def admin_section? @@ -250,11 +248,7 @@ module ApplicationHelper end def stylesheet_link_tag_defer(path) - if use_startup_css? - stylesheet_link_tag(path, media: "print", crossorigin: ActionController::Base.asset_host ? 'anonymous' : nil) - else - stylesheet_link_tag(path, media: "all") - end + stylesheet_link_tag(path, media: "print", crossorigin: ActionController::Base.asset_host ? 'anonymous' : nil) end def outdated_browser? diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 7866e3e3d9f..ed30adfabf0 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -242,6 +242,7 @@ module ApplicationSettingsHelper :housekeeping_incremental_repack_period, :html_emails_enabled, :import_sources, + :invisible_captcha_enabled, :max_artifacts_size, :max_attachment_size, :max_import_size, @@ -335,7 +336,8 @@ module ApplicationSettingsHelper :group_export_limit, :group_download_export_limit, :wiki_page_max_content_bytes, - :container_registry_delete_tags_service_timeout + :container_registry_delete_tags_service_timeout, + :rate_limiting_response_text ] end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 0c5823894c5..bca53dfb88a 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -106,11 +106,12 @@ module BlobHelper return unless blob common_classes = "btn btn-#{btn_class}" + base_button = button_tag(label, class: "#{common_classes} disabled", disabled: true) if !on_top_of_branch?(project, ref) - button_tag label, class: "#{common_classes} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' } + modify_file_button_tooltip(base_button, _("You can only %{action} files when you are on a branch") % { action: action }) elsif blob.stored_externally? - button_tag label, class: "#{common_classes} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' } + modify_file_button_tooltip(base_button, _("It is not possible to %{action} files that are stored in LFS using the web interface") % { action: action }) elsif can_modify_blob?(blob, project, ref) button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal' elsif can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project) @@ -217,6 +218,10 @@ module BlobHelper @gitlab_ci_ymls ||= template_dropdown_names(TemplateFinder.build(:gitlab_ci_ymls, project).execute) end + def gitlab_ci_syntax_ymls(project) + @gitlab_ci_syntax_ymls ||= template_dropdown_names(TemplateFinder.build(:gitlab_ci_syntax_ymls, project).execute) + end + def metrics_dashboard_ymls(project) @metrics_dashboard_ymls ||= template_dropdown_names(TemplateFinder.build(:metrics_dashboard_ymls, project).execute) end @@ -360,7 +365,11 @@ module BlobHelper end def edit_disabled_button_tag(button_text, common_classes) - button_tag(button_text, class: "#{common_classes} disabled has-tooltip", title: _('You can only edit files when you are on a branch'), data: { container: 'body' }) + button = button_tag(button_text, class: "#{common_classes} disabled", disabled: true) + + # Disabled buttons with tooltips should have the tooltip attached + # to a wrapper element https://bootstrap-vue.org/docs/components/tooltip#disabled-elements + content_tag(:span, button, class: 'has-tooltip', title: _('You can only edit files when you are on a branch'), data: { container: 'body' }) end def edit_link_tag(link_text, edit_path, common_classes, data) @@ -397,4 +406,12 @@ module BlobHelper @path.to_s.end_with?(Ci::Pipeline::CONFIG_EXTENSION) || @path.to_s == @project.ci_config_path_or_default end + + private + + def modify_file_button_tooltip(button, tooltip_message) + # Disabled buttons with tooltips should have the tooltip attached + # to a wrapper element https://bootstrap-vue.org/docs/components/tooltip#disabled-elements + content_tag(:span, button, class: 'btn-group has-tooltip', title: tooltip_message, data: { container: 'body' }) + end end diff --git a/app/helpers/ci/pipelines_helper.rb b/app/helpers/ci/pipelines_helper.rb index 309aa477108..8a6f0821dbb 100644 --- a/app/helpers/ci/pipelines_helper.rb +++ b/app/helpers/ci/pipelines_helper.rb @@ -26,6 +26,10 @@ module Ci _("%{message} showing first %{warnings_displayed}") % { message: message, warnings_displayed: MAX_LIMIT } end + def has_gitlab_ci?(project) + project.has_ci? && project.builds_enabled? + end + private def warning_markdown(pipeline) diff --git a/app/helpers/triggers_helper.rb b/app/helpers/ci/triggers_helper.rb index 5cfdc0971f0..01555b6e2cc 100644 --- a/app/helpers/triggers_helper.rb +++ b/app/helpers/ci/triggers_helper.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module TriggersHelper +module Ci::TriggersHelper def builds_trigger_url(project_id, ref: nil) if ref.nil? "#{Settings.gitlab.url}/api/v4/projects/#{project_id}/trigger/pipeline" diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 02d48386e31..e6e2b5b128b 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -110,8 +110,16 @@ module CommitsHelper end end - def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) - commit_action_link('revert', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip) + def revert_commit_link(commit, continue_to_path, btn_class: nil, pajamas: false) + return unless current_user + + action = 'revert' + + if pajamas && can_collaborate_with_project?(@project) + tag(:div, data: { display_text: action.capitalize }, class: "js-revert-commit-trigger") + else + commit_action_link(action, commit, continue_to_path, btn_class: btn_class, has_tooltip: false) + end end def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb index 195b3162039..08f357916b5 100644 --- a/app/helpers/dashboard_helper.rb +++ b/app/helpers/dashboard_helper.rb @@ -11,6 +11,10 @@ module DashboardHelper merge_requests_dashboard_path(assignee_username: current_user.username) end + def reviewer_mrs_dashboard_path + merge_requests_dashboard_path(reviewer_username: current_user.username) + end + def dashboard_nav_links @dashboard_nav_links ||= get_dashboard_nav_links end diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 017981c8c8e..b58ff21b257 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -214,6 +214,29 @@ module EmailsHelper end end + def group_membership_expiration_changed_text(member, group) + if member.expires? + days = (member.expires_at - Date.today).to_i + days_formatted = pluralize(days, 'day') + + _('Your %{group} membership will now expire in %{days}.') % { group: group.human_name, days: days_formatted } + else + _('Your membership in %{group} no longer expires.') % { group: group.human_name } + end + end + + def group_membership_expiration_changed_link(member, group, format: nil) + url = group_group_members_url(group, search: member.user.username) + + case format + when :html + link_to = generate_link('group membership', url).html_safe + _('For additional information, review your %{link_to} or contact your group owner.').html_safe % { link_to: link_to } + else + _('For additional information, review your group membership: %{link_to} or contact your group owner.') % { link_to: url } + end + end + def instance_access_request_text(user, format: nil) gitlab_host = Gitlab.config.gitlab.host diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb index 24072d1ab46..abb3c5a7af8 100644 --- a/app/helpers/graph_helper.rb +++ b/app/helpers/graph_helper.rb @@ -22,4 +22,10 @@ module GraphHelper ratio = (counts[:success].to_f / (counts[:success] + counts[:failed])) * 100 ratio.to_i end + + def should_render_deployment_frequency_charts + false + end end + +GraphHelper.prepend_if_ee('EE::GraphHelper') diff --git a/app/helpers/groups/group_members_helper.rb b/app/helpers/groups/group_members_helper.rb index adc9d85a384..a4159ed6b19 100644 --- a/app/helpers/groups/group_members_helper.rb +++ b/app/helpers/groups/group_members_helper.rb @@ -18,7 +18,7 @@ module Groups::GroupMembersHelper end def members_data_json(group, members) - members_data(group, members).to_json + MemberSerializer.new.represent(members, { current_user: current_user, group: group }).to_json end # Overridden in `ee/app/helpers/ee/groups/group_members_helper.rb` @@ -38,84 +38,6 @@ module Groups::GroupMembersHelper group_id: group.id } end - - private - - def members_data(group, members) - members.map do |member| - user = member.user - source = member.source - - data = { - id: member.id, - created_at: member.created_at, - expires_at: member.expires_at&.to_time, - requested_at: member.requested_at, - can_update: member.can_update?, - can_remove: member.can_remove?, - access_level: { - string_value: member.human_access, - integer_value: member.access_level - }, - source: { - id: source.id, - name: source.full_name, - web_url: Gitlab::UrlBuilder.build(source) - }, - valid_roles: member.valid_level_roles - }.merge(member_created_by_data(member.created_by)) - - if member.invite? - data[:invite] = member_invite_data(member) - elsif user.present? - data[:user] = member_user_data(user) - end - - data - end - end - - def member_created_by_data(created_by) - return {} unless created_by.present? - - { - created_by: { - name: created_by.name, - web_url: Gitlab::UrlBuilder.build(created_by) - } - } - end - - def member_user_data(user) - { - id: user.id, - name: user.name, - username: user.username, - web_url: Gitlab::UrlBuilder.build(user), - avatar_url: avatar_icon_for_user(user, AVATAR_SIZE), - blocked: user.blocked?, - two_factor_enabled: user.two_factor_enabled? - }.merge(member_user_status_data(user.status)) - end - - def member_user_status_data(status) - return {} unless status.present? - - { - status: { - emoji: status.emoji, - message_html: status.message_html - } - } - end - - def member_invite_data(member) - { - email: member.invite_email, - avatar_url: avatar_icon_for_email(member.invite_email, AVATAR_SIZE), - can_resend: member.can_resend_invite? - } - end end Groups::GroupMembersHelper.prepend_if_ee('EE::Groups::GroupMembersHelper') diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index e8eb6a5d417..133d9d21a14 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -22,6 +22,7 @@ module GroupsHelper ldap_group_links#index hooks#index pipeline_quota#index + packages_and_registries#index ] end diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb index cea28fd4611..a643fea6d5a 100644 --- a/app/helpers/invite_members_helper.rb +++ b/app/helpers/invite_members_helper.rb @@ -22,4 +22,34 @@ module InviteMembersHelper def invite_group_members?(group) experiment_enabled?(:invite_members_empty_group_version_a) && Ability.allowed?(current_user, :admin_group_member, group) end + + def dropdown_invite_members_link(form_model) + link_to invite_members_url(form_model), + data: { + 'track-event': 'click_link', + 'track-label': tracking_label(current_user), + 'track-property': experiment_tracking_category_and_group(:invite_members_new_dropdown, subject: current_user) + } do + invite_member_link_content + end + end + + private + + def invite_members_url(form_model) + case form_model + when Project + project_project_members_path(form_model) + when Group + group_group_members_path(form_model) + end + end + + def invite_member_link_content + text = s_('InviteMember|Invite members') + + return text unless experiment_enabled?(:invite_members_new_dropdown) + + "#{text} #{emoji_icon('shaking_hands', 'aria-hidden': true, class: 'gl-font-base gl-vertical-align-baseline')}".html_safe + end end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 15842dec3dd..da142cbed0e 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -442,7 +442,8 @@ module IssuablesHelper fullPath: issuable[:project_full_path], iid: issuable[:iid], severity: issuable[:severity], - timeTrackingLimitToHours: Gitlab::CurrentSettings.time_tracking_limit_to_hours + timeTrackingLimitToHours: Gitlab::CurrentSettings.time_tracking_limit_to_hours, + createNoteEmail: issuable[:create_note_email] } end diff --git a/app/helpers/jira_connect_helper.rb b/app/helpers/jira_connect_helper.rb new file mode 100644 index 00000000000..f1527b9b85a --- /dev/null +++ b/app/helpers/jira_connect_helper.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module JiraConnectHelper + def new_jira_connect_ui? + Feature.enabled?(:new_jira_connect_ui, type: :development, default_enabled: :yaml) + end + + def jira_connect_app_data + { + groups_path: api_v4_groups_path(params: { min_access_level: Gitlab::Access::MAINTAINER }) + } + end +end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 37e701c1c2b..6ab5f499b9a 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -159,6 +159,36 @@ module MergeRequestsHelper issuable_path(issuable, { merge_request: { wip_event: wip_event } }) end + + def user_merge_requests_counts + @user_merge_requests_counts ||= begin + assigned_count = assigned_issuables_count(:merge_requests) + review_requested_count = review_requested_merge_requests_count + total_count = assigned_count + review_requested_count + + { + assigned: assigned_count, + review_requested: review_requested_count, + total: total_count + } + end + end + + def merge_request_reviewers_enabled? + Feature.enabled?(:merge_request_reviewers, default_enabled: :yaml) + end + + private + + def review_requested_merge_requests_count + return 0 unless merge_request_reviewers_enabled? + + current_user.review_requested_open_merge_requests_count + end + + def default_suggestion_commit_message + @project.suggestion_commit_message.presence || Gitlab::Suggestions::CommitMessage::DEFAULT_SUGGESTION_COMMIT_MESSAGE + end end MergeRequestsHelper.prepend_if_ee('EE::MergeRequestsHelper') diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index e3d82e7a091..8920133734c 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -159,7 +159,7 @@ module PageLayoutHelper end def user_status_properties(user) - default_properties = { current_emoji: '', current_message: '', can_set_user_availability: Feature.enabled?(:set_user_availability_status, user), default_emoji: UserStatus::DEFAULT_EMOJI } + default_properties = { current_emoji: '', current_message: '', can_set_user_availability: Feature.enabled?(:set_user_availability_status, user, default_enabled: :yaml), default_emoji: UserStatus::DEFAULT_EMOJI } return default_properties unless user&.status default_properties.merge({ diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 5310aef5bad..c2a77abb9c9 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -82,7 +82,7 @@ module PreferencesHelper def integration_views [].tap do |views| - views << { name: 'gitpod', message: gitpod_enable_description, message_url: 'https://gitpod.io/', help_link: help_page_path('integration/gitpod.md') } if Gitlab::Gitpod.feature_and_settings_enabled? + views << { name: 'gitpod', message: gitpod_enable_description, message_url: 'https://gitpod.io/', help_link: help_page_path('integration/gitpod.md') } if Gitlab::CurrentSettings.gitpod_enabled views << { name: 'sourcegraph', message: sourcegraph_url_message, message_url: Gitlab::CurrentSettings.sourcegraph_url, help_link: help_page_path('user/profile/preferences.md', anchor: 'sourcegraph') } if Gitlab::Sourcegraph.feature_available? && Gitlab::CurrentSettings.sourcegraph_enabled end end diff --git a/app/helpers/projects/alert_management_helper.rb b/app/helpers/projects/alert_management_helper.rb index 997551d9659..58f1abb2818 100644 --- a/app/helpers/projects/alert_management_helper.rb +++ b/app/helpers/projects/alert_management_helper.rb @@ -34,5 +34,3 @@ module Projects::AlertManagementHelper ) end end - -Projects::AlertManagementHelper.prepend_if_ee('EE::Projects::AlertManagementHelper') diff --git a/app/helpers/projects/project_members_helper.rb b/app/helpers/projects/project_members_helper.rb new file mode 100644 index 00000000000..168526d2abb --- /dev/null +++ b/app/helpers/projects/project_members_helper.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Projects::ProjectMembersHelper + def can_manage_project_members?(project) + can?(current_user, :admin_project_member, project) + end + + def show_groups?(group_links) + group_links.exists? || groups_tab_active? + end + + def show_invited_members?(project, invited_members) + can_manage_project_members?(project) && invited_members.exists? + end + + def show_access_requests?(project, requesters) + can_manage_project_members?(project) && requesters.exists? + end + + def groups_tab_active? + params[:search_groups].present? + end + + def current_user_is_group_owner?(project) + return false if project.group.nil? + + project.group.has_owner?(current_user) + end +end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 80206654cd1..b21d3ca51db 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -150,13 +150,7 @@ module ProjectsHelper end def can_change_visibility_level?(project, current_user) - return false unless can?(current_user, :change_visibility_level, project) - - if project.fork_source - project.fork_source.visibility_level > Gitlab::VisibilityLevel::PRIVATE - else - true - end + can?(current_user, :change_visibility_level, project) end def can_disable_emails?(project, current_user) diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb index 3516000e296..5f361e6653d 100644 --- a/app/helpers/services_helper.rb +++ b/app/helpers/services_helper.rb @@ -76,6 +76,8 @@ module ServicesHelper end def scoped_reset_integration_path(integration, group: nil) + return '' unless integration.persisted? + if group.present? reset_group_settings_integration_path(group, integration) else @@ -102,7 +104,7 @@ module ServicesHelper cancel_path: scoped_integrations_path, can_test: integration.can_test?.to_s, test_path: scoped_test_integration_path(integration), - reset_path: reset_integration?(integration, group: group) ? scoped_reset_integration_path(integration, group: group) : '' + reset_path: scoped_reset_integration_path(integration, group: group) } end @@ -126,10 +128,6 @@ module ServicesHelper !Gitlab.com? end - def reset_integration?(integration, group: nil) - integration.persisted? && Feature.enabled?(:reset_integrations, group, type: :development) - end - extend self private diff --git a/app/helpers/startup_css_helper.rb b/app/helpers/startup_css_helper.rb deleted file mode 100644 index b54e19bfc0d..00000000000 --- a/app/helpers/startup_css_helper.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -module StartupCssHelper - def use_startup_css? - (Feature.enabled?(:startup_css) || params[:startup_css] == 'true' || cookies['startup_css'] == 'true') && !Rails.env.test? - end -end diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index 6fbe2642056..e81986d4453 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -72,7 +72,8 @@ module TabHelper # Add our custom class into the html_options, which may or may not exist # and which may or may not already have a :class key o = options.delete(:html_options) || {} - o[:class] = [*o[:class], klass].join(' ').strip + o[:class] = [*o[:class], klass].join(' ') + o[:class].strip! if block_given? content_tag(:li, capture(&block), o) diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index a58f8a6f792..a5d4d6872df 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -3,7 +3,7 @@ module UsersHelper def admin_users_data_attributes(users) { - users: Admin::UserSerializer.new.represent(users).to_json, + users: Admin::UserSerializer.new.represent(users, { current_user: current_user }).to_json, paths: admin_users_paths.to_json } end diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index 0a37257e124..696f29164fd 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -26,76 +26,6 @@ module VisibilityLevelHelper end end - def project_visibility_level_description(level) - case level - when Gitlab::VisibilityLevel::PRIVATE - _("Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group.") - when Gitlab::VisibilityLevel::INTERNAL - _("The project can be accessed by any logged in user except external users.") - when Gitlab::VisibilityLevel::PUBLIC - _("The project can be accessed without any authentication.") - end - end - - def group_visibility_level_description(level) - case level - when Gitlab::VisibilityLevel::PRIVATE - _("The group and its projects can only be viewed by members.") - when Gitlab::VisibilityLevel::INTERNAL - _("The group and any internal projects can be viewed by any logged in user except external users.") - when Gitlab::VisibilityLevel::PUBLIC - _("The group and any public projects can be viewed without any authentication.") - end - end - - # Note: these messages closely mirror the form validation strings found in the project - # model and any changes or additons to these may also need to be made there. - def disallowed_project_visibility_level_description(level, project) - level_name = Gitlab::VisibilityLevel.level_name(level).downcase - reasons = [] - instructions = [] - - unless project.visibility_level_allowed_as_fork?(level) - reasons << "the fork source project has lower visibility" - end - - unless project.visibility_level_allowed_by_group?(level) - errors = visibility_level_errors_for_group(project.group, level_name) - - reasons << errors[:reason] - instructions << errors[:instruction] - end - - reasons = reasons.any? ? ' because ' + reasons.to_sentence : '' - "This project cannot be #{level_name}#{reasons}.#{instructions.join}".html_safe - end - - # Note: these messages closely mirror the form validation strings found in the group - # model and any changes or additons to these may also need to be made there. - def disallowed_group_visibility_level_description(level, group) - level_name = Gitlab::VisibilityLevel.level_name(level).downcase - reasons = [] - instructions = [] - - unless group.visibility_level_allowed_by_projects?(level) - reasons << "it contains projects with higher visibility" - end - - unless group.visibility_level_allowed_by_sub_groups?(level) - reasons << "it contains sub-groups with higher visibility" - end - - unless group.visibility_level_allowed_by_parent?(level) - errors = visibility_level_errors_for_group(group.parent, level_name) - - reasons << errors[:reason] - instructions << errors[:instruction] - end - - reasons = reasons.any? ? ' because ' + reasons.to_sentence : '' - "This group cannot be #{level_name}#{reasons}.#{instructions.join}".html_safe - end - def visibility_icon_description(form_model) if form_model.respond_to?(:visibility_level_allowed_as_fork?) project_visibility_icon_description(form_model.visibility_level) @@ -104,14 +34,6 @@ module VisibilityLevelHelper end end - def group_visibility_icon_description(level) - "#{visibility_level_label(level)} - #{group_visibility_level_description(level)}" - end - - def project_visibility_icon_description(level) - "#{visibility_level_label(level)} - #{project_visibility_level_description(level)}" - end - def visibility_level_label(level) # The visibility level can be: # 'VisibilityLevel|Private', 'VisibilityLevel|Internal', 'VisibilityLevel|Public' @@ -203,11 +125,33 @@ module VisibilityLevelHelper current_level end - def visibility_level_errors_for_group(group, level_name) - group_name = link_to group.name, group_path(group) - change_visibility = link_to 'change the visibility', edit_group_path(group) + def project_visibility_level_description(level) + case level + when Gitlab::VisibilityLevel::PRIVATE + _("Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group.") + when Gitlab::VisibilityLevel::INTERNAL + _("The project can be accessed by any logged in user except external users.") + when Gitlab::VisibilityLevel::PUBLIC + _("The project can be accessed without any authentication.") + end + end + + def group_visibility_level_description(level) + case level + when Gitlab::VisibilityLevel::PRIVATE + _("The group and its projects can only be viewed by members.") + when Gitlab::VisibilityLevel::INTERNAL + _("The group and any internal projects can be viewed by any logged in user except external users.") + when Gitlab::VisibilityLevel::PUBLIC + _("The group and any public projects can be viewed without any authentication.") + end + end - { reason: "the visibility of #{group_name} is #{group.visibility}", - instruction: " To make this group #{level_name}, you must first #{change_visibility} of the parent group." } + def project_visibility_icon_description(level) + "#{visibility_level_label(level)} - #{project_visibility_level_description(level)}" + end + + def group_visibility_icon_description(level) + "#{visibility_level_label(level)} - #{group_visibility_level_description(level)}" end end diff --git a/app/helpers/web_ide_button_helper.rb b/app/helpers/web_ide_button_helper.rb index 7aa0adc31bd..6c73d365e8e 100644 --- a/app/helpers/web_ide_button_helper.rb +++ b/app/helpers/web_ide_button_helper.rb @@ -26,7 +26,7 @@ module WebIdeButtonHelper end def show_gitpod_button? - show_web_ide_button? && Gitlab::Gitpod.feature_and_settings_enabled?(@project) + show_web_ide_button? && Gitlab::CurrentSettings.gitpod_enabled end def can_push_code? @@ -54,7 +54,7 @@ module WebIdeButtonHelper end def gitpod_url - return "" unless Gitlab::Gitpod.feature_and_settings_enabled?(@project) + return "" unless Gitlab::CurrentSettings.gitpod_enabled "#{Gitlab::CurrentSettings.gitpod_url}##{project_tree_url(@project, tree_join(@ref, @path || ''))}" end diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index b2c1351bd28..c565df1a2ee 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -106,6 +106,8 @@ module Emails @count = export_status.fetch(:rows_expected) @written_count = export_status.fetch(:rows_written) @truncated = export_status.fetch(:truncated) + @size_limit = ActiveSupport::NumberHelper + .number_to_human_size(Issuable::ExportCsv::BaseService::TARGET_FILESIZE) filename = "#{project.full_path.parameterize}_issues_#{Date.today.iso8601}.csv" attachments[filename] = { content: csv_data, mime_type: 'text/csv' } diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb index 759181bd3cb..69f5fe1430a 100644 --- a/app/mailers/emails/members.rb +++ b/app/mailers/emails/members.rb @@ -114,6 +114,23 @@ module Emails subject: subject('Invitation declined')) end + def member_expiration_date_updated_email(member_source_type, member_id) + @member_source_type = member_source_type + @member_id = member_id + + return unless member_exists? + + subject = if member.expires? + _('Group membership expiration date changed') + else + _('Group membership expiration date removed') + end + + member_email_with_layout( + to: member.user.notification_email_for(notification_group), + subject: subject(subject)) + end + # rubocop: disable CodeReuse/ActiveRecord def member @member ||= Member.find_by(id: @member_id) @@ -147,3 +164,5 @@ module Emails end end end + +Emails::Members.prepend_if_ee('EE::Emails::Members') diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 28ac752f550..4faa1a11276 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -115,6 +115,8 @@ module Emails @count = export_status.fetch(:rows_expected) @written_count = export_status.fetch(:rows_written) @truncated = export_status.fetch(:truncated) + @size_limit = ActiveSupport::NumberHelper + .number_to_human_size(Issuable::ExportCsv::BaseService::TARGET_FILESIZE) filename = "#{project.full_path.parameterize}_merge_requests_#{Date.current.iso8601}.csv" attachments[filename] = { content: csv_data, mime_type: 'text/csv' } diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb index f2538d28a1a..0b830f4ee5e 100644 --- a/app/mailers/emails/pipelines.rb +++ b/app/mailers/emails/pipelines.rb @@ -3,15 +3,15 @@ module Emails module Pipelines def pipeline_success_email(pipeline, recipients) - pipeline_mail(pipeline, recipients, 'succeeded') + pipeline_mail(pipeline, recipients, 'Succesful') end def pipeline_failed_email(pipeline, recipients) - pipeline_mail(pipeline, recipients, 'failed') + pipeline_mail(pipeline, recipients, 'Failed') end def pipeline_fixed_email(pipeline, recipients) - pipeline_mail(pipeline, recipients, 'been fixed') + pipeline_mail(pipeline, recipients, 'Fixed') end private @@ -50,10 +50,13 @@ module Emails end def pipeline_subject(status) - commit = [@pipeline.short_sha] - commit << "in #{@merge_request.to_reference}" if @merge_request + subject = [] - subject("Pipeline ##{@pipeline.id} has #{status} for #{@pipeline.source_ref}", commit.join(' ')) + subject << "#{status} pipeline for #{@pipeline.source_ref}" + subject << @project.name if @project + subject << @pipeline.short_sha + + subject.join(' | ') end end end diff --git a/app/mailers/previews/devise_mailer_preview.rb b/app/mailers/previews/devise_mailer_preview.rb index 68f281f825e..2919d466073 100644 --- a/app/mailers/previews/devise_mailer_preview.rb +++ b/app/mailers/previews/devise_mailer_preview.rb @@ -31,6 +31,6 @@ class DeviseMailerPreview < ActionMailer::Preview private def unsaved_user - User.new(name: 'Jane Doe', email: 'jdoe@example.com') + User.new(name: 'Jane Doe', email: 'jdoe@example.com', created_at: 1.minute.ago) end end diff --git a/app/models/alert_management/http_integration.rb b/app/models/alert_management/http_integration.rb index 0c916c576cb..2d9a2d7031c 100644 --- a/app/models/alert_management/http_integration.rb +++ b/app/models/alert_management/http_integration.rb @@ -52,6 +52,10 @@ module AlertManagement endpoint_identifier == LEGACY_IDENTIFIER end + def token_changed? + attribute_changed?(:token) + end + # Blank token assignment triggers token reset def prevent_token_assignment if token.present? && token_changed? diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 71235ed1002..44d1b6cf907 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -77,4 +77,9 @@ class ApplicationRecord < ActiveRecord::Base def self.where_exists(query) where('EXISTS (?)', query.select(1)) end + + def self.declarative_enum(enum_mod) + values = enum_mod.definition.transform_values { |v| v[:value] } + enum(enum_mod.key => values) + end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 9b9db7f93fd..5655ea4d4bf 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -171,7 +171,7 @@ class ApplicationSetting < ApplicationRecord validates :default_artifacts_expire_in, presence: true, duration: true validates :container_expiration_policies_enable_historic_entries, - inclusion: { in: [true, false], message: 'must be a boolean value' } + inclusion: { in: [true, false], message: _('must be a boolean value') } validates :container_registry_token_expire_delay, presence: true, @@ -303,9 +303,15 @@ class ApplicationSetting < ApplicationRecord validates :container_registry_delete_tags_service_timeout, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :container_registry_cleanup_tags_service_max_list_size, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :container_registry_expiration_policies_worker_capacity, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :invisible_captcha_enabled, + inclusion: { in: [true, false], message: _('must be a boolean value') } + SUPPORTED_KEY_TYPES.each do |type| validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } end @@ -400,6 +406,42 @@ class ApplicationSetting < ApplicationRecord validates :ci_jwt_signing_key, rsa_key: true, allow_nil: true + validates :rate_limiting_response_text, + length: { maximum: 255, message: _('is too long (maximum is %{count} characters)') }, + allow_blank: true + + validates :throttle_unauthenticated_requests_per_period, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + + validates :throttle_unauthenticated_period_in_seconds, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + + validates :throttle_authenticated_api_requests_per_period, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + + validates :throttle_authenticated_api_period_in_seconds, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + + validates :throttle_authenticated_web_requests_per_period, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + + validates :throttle_authenticated_web_period_in_seconds, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + + validates :throttle_protected_paths_requests_per_period, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + + validates :throttle_protected_paths_period_in_seconds, + presence: true, + numericality: { only_integer: true, greater_than: 0 } + attr_encrypted :asset_proxy_secret_key, mode: :per_attribute_iv, key: Settings.attr_encrypted_db_key_base_truncated, @@ -430,7 +472,7 @@ class ApplicationSetting < ApplicationRecord attr_encrypted :cloud_license_auth_token, encryption_options_base_truncated_aes_256_gcm validates :disable_feed_token, - inclusion: { in: [true, false], message: 'must be a boolean value' } + inclusion: { in: [true, false], message: _('must be a boolean value') } before_validation :ensure_uuid! diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 105889a364a..b05355f14b4 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -91,12 +91,13 @@ module ApplicationSettingImplementation housekeeping_gc_period: 200, housekeeping_incremental_repack_period: 10, import_sources: Settings.gitlab['import_sources'], + invisible_captcha_enabled: false, issues_create_limit: 300, local_markdown_version: 0, login_recaptcha_protection_enabled: false, max_artifacts_size: Settings.artifacts['max_size'], max_attachment_size: Settings.gitlab['max_attachment_size'], - max_import_size: 50, + max_import_size: 0, minimum_password_length: DEFAULT_MINIMUM_PASSWORD_LENGTH, mirror_available: true, notify_on_unknown_sign_in: true, @@ -172,7 +173,8 @@ module ApplicationSettingImplementation container_registry_delete_tags_service_timeout: 250, container_registry_expiration_policies_worker_capacity: 0, kroki_enabled: false, - kroki_url: nil + kroki_url: nil, + rate_limiting_response_text: nil } end diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb index a4d991b040c..d1c0bb11dc8 100644 --- a/app/models/audit_event.rb +++ b/app/models/audit_event.rb @@ -4,6 +4,7 @@ class AuditEvent < ApplicationRecord include CreatedAtFilterable include BulkInsertSafe include EachBatch + include PartitionedTable PARALLEL_PERSISTENCE_COLUMNS = [ :author_name, @@ -15,6 +16,8 @@ class AuditEvent < ApplicationRecord self.primary_key = :id + partitioned_by :created_at, strategy: :monthly + serialize :details, Hash # rubocop:disable Cop/ActiveRecordSerialize belongs_to :user, foreign_key: :author_id diff --git a/app/models/audit_event_archived.rb b/app/models/audit_event_archived.rb new file mode 100644 index 00000000000..3119f56fbcc --- /dev/null +++ b/app/models/audit_event_archived.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# This model is not intended to be used. +# It is a temporary reference to the pre-partitioned +# audit_events table. +# Please refer to https://gitlab.com/groups/gitlab-org/-/epics/3206 +# for details. +class AuditEventArchived < ApplicationRecord + self.table_name = 'audit_events_archived' +end diff --git a/app/models/audit_event_partitioned.rb b/app/models/audit_event_partitioned.rb deleted file mode 100644 index 672daebd14a..00000000000 --- a/app/models/audit_event_partitioned.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -# This model is not yet intended to be used. -# It is in a transitioning phase while we are partitioning -# the table on the database-side. -# Please refer to https://gitlab.com/groups/gitlab-org/-/epics/3206 -# for details. -class AuditEventPartitioned < ApplicationRecord - include PartitionedTable - - self.table_name = 'audit_events_part_5fc467ac26' - - partitioned_by :created_at, strategy: :monthly -end diff --git a/app/models/authentication_event.rb b/app/models/authentication_event.rb index 9d191e6ae4d..1e822629ba1 100644 --- a/app/models/authentication_event.rb +++ b/app/models/authentication_event.rb @@ -3,10 +3,10 @@ class AuthenticationEvent < ApplicationRecord include UsageStatistics - TWO_FACTOR = 'two-factor'.freeze - TWO_FACTOR_U2F = 'two-factor-via-u2f-device'.freeze - TWO_FACTOR_WEBAUTHN = 'two-factor-via-webauthn-device'.freeze - STANDARD = 'standard'.freeze + TWO_FACTOR = 'two-factor' + TWO_FACTOR_U2F = 'two-factor-via-u2f-device' + TWO_FACTOR_WEBAUTHN = 'two-factor-via-webauthn-device' + STANDARD = 'standard' STATIC_PROVIDERS = [TWO_FACTOR, TWO_FACTOR_U2F, TWO_FACTOR_WEBAUTHN, STANDARD].freeze belongs_to :user, optional: true diff --git a/app/models/blob_viewer/gitlab_ci_yml.rb b/app/models/blob_viewer/gitlab_ci_yml.rb index 11228e620c9..e255b6d15d2 100644 --- a/app/models/blob_viewer/gitlab_ci_yml.rb +++ b/app/models/blob_viewer/gitlab_ci_yml.rb @@ -15,7 +15,9 @@ module BlobViewer prepare! - @validation_message = Gitlab::Ci::YamlProcessor.validation_message(blob.data, opts) + @validation_message = Gitlab::Ci::Lint + .new(project: opts[:project], current_user: opts[:user], sha: opts[:sha]) + .validate(blob.data).errors.first end def valid?(opts) diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index 19a0d424e33..ef3891908f7 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -7,7 +7,6 @@ module Ci include Importable include AfterCommitQueue include Ci::HasRef - extend ::Gitlab::Utils::Override InvalidBridgeTypeError = Class.new(StandardError) InvalidTransitionError = Class.new(StandardError) @@ -200,13 +199,6 @@ module Ci end end - override :dependency_variables - def dependency_variables - return [] unless ::Feature.enabled?(:ci_bridge_dependency_variables, project, default_enabled: true) - - super - end - def target_revision_ref downstream_pipeline_params.dig(:target_revision, :ref) end @@ -218,7 +210,8 @@ module Ci project: downstream_project, source: :pipeline, target_revision: { - ref: target_ref || downstream_project.default_branch + ref: target_ref || downstream_project.default_branch, + variables_attributes: downstream_variables }, execute_params: { ignore_skip_ci: true, @@ -238,7 +231,8 @@ module Ci checkout_sha: parent_pipeline.sha, before: parent_pipeline.before_sha, source_sha: parent_pipeline.source_sha, - target_sha: parent_pipeline.target_sha + target_sha: parent_pipeline.target_sha, + variables_attributes: downstream_variables }, execute_params: { ignore_skip_ci: true, diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 71939f070cb..5e3f42d7c2c 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -27,7 +27,8 @@ module Ci upload_multiple_artifacts: -> (build) { build.publishes_artifacts_reports? }, refspecs: -> (build) { build.merge_request_ref? }, artifacts_exclude: -> (build) { build.supports_artifacts_exclude? }, - multi_build_steps: -> (build) { build.multi_build_steps? } + multi_build_steps: -> (build) { build.multi_build_steps? }, + return_exit_code: -> (build) { build.exit_codes_defined? } }.freeze DEFAULT_RETRIES = { @@ -146,6 +147,12 @@ module Ci .includes(:metadata, :job_artifacts_metadata) end + scope :with_project_and_metadata, -> do + if Feature.enabled?(:non_public_artifacts, type: :development) + joins(:metadata).includes(:project, :metadata) + end + end + scope :with_artifacts_not_expired, -> { with_downloadable_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.current) } scope :with_expired_artifacts, -> { with_downloadable_artifacts.where('artifacts_expire_at < ?', Time.current) } scope :last_month, -> { where('created_at > ?', Date.today - 1.month) } @@ -382,12 +389,8 @@ module Ci end after_transition any => [:skipped, :canceled] do |build, transition| - if Feature.enabled?(:cd_skipped_deployment_status, build.project) - if transition.to_name == :skipped - build.deployment&.skip - else - build.deployment&.cancel - end + if transition.to_name == :skipped + build.deployment&.skip else build.deployment&.cancel end @@ -741,6 +744,16 @@ module Ci artifacts_metadata? end + def artifacts_public? + return true unless Feature.enabled?(:non_public_artifacts, type: :development) + + artifacts_public = options.dig(:artifacts, :public) + + return true if artifacts_public.nil? # Default artifacts:public to true + + options.dig(:artifacts, :public) + end + def artifacts_metadata_entry(path, **options) artifacts_metadata.open do |metadata_stream| metadata = Gitlab::Ci::Build::Artifacts::Metadata.new( @@ -1007,14 +1020,23 @@ module Ci end def debug_mode? - return false unless Feature.enabled?(:restrict_access_to_build_debug_mode, default_enabled: true) - # TODO: Have `debug_mode?` check against data on sent back from runner # to capture all the ways that variables can be set. # See (https://gitlab.com/gitlab-org/gitlab/-/issues/290955) variables.any? { |variable| variable[:key] == 'CI_DEBUG_TRACE' && variable[:value].casecmp('true') == 0 } end + def drop_with_exit_code!(failure_reason, exit_code) + transaction do + conditionally_allow_failure!(exit_code) + drop!(failure_reason) + end + end + + def exit_codes_defined? + options.dig(:allow_failure_criteria, :exit_codes).present? + end + protected def run_status_commit_hooks! @@ -1098,6 +1120,22 @@ module Ci Gitlab::ErrorTracking.track_exception(e) end end + + def conditionally_allow_failure!(exit_code) + return unless ::Gitlab::Ci::Features.allow_failure_with_exit_codes_enabled? + return unless exit_code + + if allowed_to_fail_with_code?(exit_code) + update_columns(allow_failure: true) + end + end + + def allowed_to_fail_with_code?(exit_code) + options + .dig(:allow_failure_criteria, :exit_codes) + .to_a + .include?(exit_code) + end end end diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb index b977a5f4419..fac615f97b9 100644 --- a/app/models/ci/build_need.rb +++ b/app/models/ci/build_need.rb @@ -6,7 +6,7 @@ module Ci include BulkInsertSafe - belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id, inverse_of: :needs + belongs_to :build, class_name: "Ci::Processable", foreign_key: :build_id, inverse_of: :needs validates :build, presence: true validates :name, presence: true, length: { maximum: 128 } diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb index bc7f17f046c..b6196048ca1 100644 --- a/app/models/ci/build_runner_session.rb +++ b/app/models/ci/build_runner_session.rb @@ -7,8 +7,8 @@ module Ci extend Gitlab::Ci::Model TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com' - DEFAULT_SERVICE_NAME = 'build'.freeze - DEFAULT_PORT_NAME = 'default_port'.freeze + DEFAULT_SERVICE_NAME = 'build' + DEFAULT_PORT_NAME = 'default_port' self.table_name = 'ci_builds_runner_session' diff --git a/app/models/ci/commit_with_pipeline.rb b/app/models/ci/commit_with_pipeline.rb new file mode 100644 index 00000000000..7f952fb77a0 --- /dev/null +++ b/app/models/ci/commit_with_pipeline.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class Ci::CommitWithPipeline < SimpleDelegator + include Presentable + + def initialize(commit) + @latest_pipelines = {} + super(commit) + end + + def pipelines + project.ci_pipelines.where(sha: sha) + end + + def last_pipeline + strong_memoize(:last_pipeline) do + pipelines.last + end + end + + def latest_pipeline(ref = nil) + @latest_pipelines.fetch(ref) do |ref| + @latest_pipelines[ref] = latest_pipeline_for_project(ref, project) + end + end + + def latest_pipeline_for_project(ref, pipeline_project) + pipeline_project.ci_pipelines.latest_pipeline_per_commit(id, ref)[id] + end + + def set_latest_pipeline_for_ref(ref, pipeline) + @latest_pipelines[ref] = pipeline + end + + def status(ref = nil) + latest_pipeline(ref)&.status + end +end diff --git a/app/models/ci/group.rb b/app/models/ci/group.rb index f0c035635b9..c7c0ec61e62 100644 --- a/app/models/ci/group.rb +++ b/app/models/ci/group.rb @@ -24,9 +24,22 @@ module Ci def status strong_memoize(:status) do + status_struct.status + end + end + + def success? + status.to_s == 'success' + end + + def has_warnings? + status_struct.warnings? + end + + def status_struct + strong_memoize(:status_struct) do Gitlab::Ci::Status::Composite .new(@jobs) - .status end end @@ -39,8 +52,13 @@ module Ci end end - def self.fabricate(project, stage) - stage.latest_statuses + # Construct a grouping of statuses for this stage. + # We allow the caller to pass in statuses for efficiency (avoiding N+1 + # queries). + def self.fabricate(project, stage, statuses = nil) + statuses ||= stage.latest_statuses + + statuses .sort_by(&:sortable_name).group_by(&:group_name) .map do |group_name, grouped_statuses| self.new(project, stage, name: group_name, jobs: grouped_statuses) diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index c80d50ea131..f13be3b3c86 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -9,6 +9,7 @@ module Ci include Sortable include Artifactable include FileStoreMounter + include EachBatch extend Gitlab::Ci::Model TEST_REPORT_FILE_TYPES = %w[junit].freeze @@ -133,6 +134,12 @@ module Ci scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) } scope :for_job_name, ->(name) { joins(:job).where(ci_builds: { name: name }) } + scope :with_job, -> do + if Feature.enabled?(:non_public_artifacts, type: :development) + joins(:job).includes(:job) + end + end + scope :with_file_types, -> (file_types) do types = self.file_types.select { |file_type| file_types.include?(file_type) }.values @@ -170,7 +177,8 @@ module Ci end scope :downloadable, -> { where(file_type: DOWNLOADABLE_TYPES) } - scope :unlocked, -> { joins(job: :pipeline).merge(::Ci::Pipeline.unlocked).order(expire_at: :desc) } + scope :unlocked, -> { joins(job: :pipeline).merge(::Ci::Pipeline.unlocked) } + scope :order_expired_desc, -> { order(expire_at: :desc) } scope :with_destroy_preloads, -> { includes(project: [:route, :statistics]) } scope :scoped_project, -> { where('ci_job_artifacts.project_id = projects.id') } diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 5e5f51d776f..4a579892e3f 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -68,8 +68,8 @@ module Ci has_many :variables, class_name: 'Ci::PipelineVariable' has_many :deployments, through: :builds has_many :environments, -> { distinct }, through: :deployments - has_many :latest_builds, -> { latest }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build' - has_many :downloadable_artifacts, -> { not_expired.downloadable }, through: :latest_builds, source: :job_artifacts + has_many :latest_builds, -> { latest.with_project_and_metadata }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build' + has_many :downloadable_artifacts, -> { not_expired.downloadable.with_job }, through: :latest_builds, source: :job_artifacts has_many :messages, class_name: 'Ci::PipelineMessage', inverse_of: :pipeline @@ -249,7 +249,7 @@ module Ci after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline| pipeline.run_after_commit do - ::Ci::Pipelines::CreateArtifactWorker.perform_async(pipeline.id) + ::Ci::PipelineArtifacts::CoverageReportWorker.perform_async(pipeline.id) end end diff --git a/app/models/ci/ref.rb b/app/models/ci/ref.rb index 6e9b8416c10..713a0bf9c45 100644 --- a/app/models/ci/ref.rb +++ b/app/models/ci/ref.rb @@ -33,6 +33,9 @@ module Ci state :still_failing, value: 5 after_transition any => [:fixed, :success] do |ci_ref| + # Do not try to unlock if no artifacts are locked + next unless ci_ref.artifacts_locked? + ci_ref.run_after_commit do Ci::PipelineSuccessUnlockArtifactsWorker.perform_async(ci_ref.last_finished_pipeline_id) end @@ -54,6 +57,10 @@ module Ci Ci::Pipeline.last_finished_for_ref_id(self.id)&.id end + def artifacts_locked? + self.pipelines.where(locked: :artifacts_locked).exists? + end + def update_status_by!(pipeline) retry_lock(self) do next unless last_finished_pipeline_id == pipeline.id diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb index d32fff14590..8560826928a 100644 --- a/app/models/clusters/applications/cert_manager.rb +++ b/app/models/clusters/applications/cert_manager.rb @@ -65,7 +65,7 @@ module Clusters end def retry_command(command) - "for i in $(seq 1 90); do #{command} && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)" + Gitlab::Kubernetes::PodCmd.retry_command(command, times: 90) end def post_delete_script diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index b1c3116d77c..7c131e031c1 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Knative < ApplicationRecord - VERSION = '0.9.0' + VERSION = '0.10.0' REPOSITORY = 'https://charts.gitlab.io' METRICS_CONFIG = 'https://gitlab.com/gitlab-org/charts/knative/-/raw/v0.9.0/vendor/istio-metrics.yml' FETCH_IP_ADDRESS_DELAY = 30.seconds diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index 1e41b6f4f31..56acac53e0b 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Runner < ApplicationRecord - VERSION = '0.23.0' + VERSION = '0.24.0' self.table_name = 'clusters_applications_runners' diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 84de5828491..e3dcd5b0d07 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -247,7 +247,7 @@ module Clusters def prevent_modification return if provided_by_user? - if api_url_changed? || token_changed? || ca_pem_changed? + if api_url_changed? || attribute_changed?(:token) || ca_pem_changed? errors.add(:base, _('Cannot modify managed Kubernetes cluster')) return false end diff --git a/app/models/commit.rb b/app/models/commit.rb index 80dd02981c1..edce9ad293e 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -37,7 +37,7 @@ class Commit cache_markdown_field :title, pipeline: :single_line cache_markdown_field :full_title, pipeline: :single_line - cache_markdown_field :description, pipeline: :commit_description + cache_markdown_field :description, pipeline: :commit_description, limit: 1.megabyte class << self def decorate(commits, container) @@ -80,7 +80,7 @@ class Commit def diff_hard_limit_files(project: nil) if Feature.enabled?(:increased_diff_limits, project) - 2000 + 3000 else 1000 end @@ -88,7 +88,7 @@ class Commit def diff_hard_limit_lines(project: nil) if Feature.enabled?(:increased_diff_limits, project) - 75000 + 100000 else 50000 end @@ -148,7 +148,7 @@ class Commit to: :with_pipeline def with_pipeline - @with_pipeline ||= CommitWithPipeline.new(self) + @with_pipeline ||= Ci::CommitWithPipeline.new(self) end def id diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index ee9c2501bfc..a399ffc32de 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -80,9 +80,9 @@ class CommitStatus < ApplicationRecord merge(or_conditions) end - # We use `Enums::CommitStatus.failure_reasons` here so that EE can more easily + # We use `Enums::Ci::CommitStatus.failure_reasons` here so that EE can more easily # extend this `Hash` with new values. - enum_with_nil failure_reason: Enums::CommitStatus.failure_reasons + enum_with_nil failure_reason: Enums::Ci::CommitStatus.failure_reasons ## # We still create some CommitStatuses outside of CreatePipelineService. @@ -159,6 +159,12 @@ class CommitStatus < ApplicationRecord commit_status.failure_reason = CommitStatus.failure_reasons[failure_reason] end + before_transition [:skipped, :manual] => :created do |commit_status, transition| + transition.args.first.try do |user| + commit_status.user = user + end + end + after_transition do |commit_status, transition| next if transition.loopback? next if commit_status.processed? @@ -203,7 +209,7 @@ class CommitStatus < ApplicationRecord def group_name # 'rspec:linux: 1/10' => 'rspec:linux' - common_name = name.to_s.gsub(%r{\d+[\s:\/\\]+\d+\s*}, '') + common_name = name.to_s.gsub(%r{\b\d+[\s:\/\\]+\d+\s*}, '') # 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux' common_name.gsub!(%r{: \[.*\]\s*\z}, '') diff --git a/app/models/commit_with_pipeline.rb b/app/models/commit_with_pipeline.rb index f382ae8f55a..7f952fb77a0 100644 --- a/app/models/commit_with_pipeline.rb +++ b/app/models/commit_with_pipeline.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class CommitWithPipeline < SimpleDelegator +class Ci::CommitWithPipeline < SimpleDelegator include Presentable def initialize(commit) diff --git a/app/models/concerns/boards/listable.rb b/app/models/concerns/boards/listable.rb new file mode 100644 index 00000000000..b7c0a8b3489 --- /dev/null +++ b/app/models/concerns/boards/listable.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Boards + module Listable + extend ActiveSupport::Concern + + included do + validates :label, :position, presence: true, if: :label? + validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :movable? + + before_destroy :can_be_destroyed + + scope :ordered, -> { order(:list_type, :position) } + scope :destroyable, -> { where(list_type: list_types.slice(*destroyable_types).values) } + scope :movable, -> { where(list_type: list_types.slice(*movable_types).values) } + end + + class_methods do + def destroyable_types + [:label] + end + + def movable_types + [:label] + end + end + + def destroyable? + self.class.destroyable_types.include?(list_type&.to_sym) + end + + def movable? + self.class.movable_types.include?(list_type&.to_sym) + end + + def title + if label? + label.name + elsif backlog? + _('Open') + else + list_type.humanize + end + end + + private + + def can_be_destroyed + throw(:abort) unless destroyable? # rubocop:disable Cop/BanCatchThrow + end + end +end diff --git a/app/models/concerns/bulk_insert_safe.rb b/app/models/concerns/bulk_insert_safe.rb index f9eb3fb875e..3748e77e933 100644 --- a/app/models/concerns/bulk_insert_safe.rb +++ b/app/models/concerns/bulk_insert_safe.rb @@ -53,9 +53,9 @@ module BulkInsertSafe class_methods do def set_callback(name, *args) unless _bulk_insert_callback_allowed?(name, args) - raise MethodNotAllowedError.new( + raise MethodNotAllowedError, "Not allowed to call `set_callback(#{name}, #{args})` when model extends `BulkInsertSafe`." \ - "Callbacks that fire per each record being inserted do not work with bulk-inserts.") + "Callbacks that fire per each record being inserted do not work with bulk-inserts." end super diff --git a/app/models/concerns/ci/artifactable.rb b/app/models/concerns/ci/artifactable.rb index 24df86dbc3c..cbe7d3b6abb 100644 --- a/app/models/concerns/ci/artifactable.rb +++ b/app/models/concerns/ci/artifactable.rb @@ -18,7 +18,8 @@ module Ci gzip: 3 }, _suffix: true - scope :expired, -> (limit) { where('expire_at < ?', Time.current).limit(limit) } + scope :expired_before, -> (timestamp) { where(arel_table[:expire_at].lt(timestamp)) } + scope :expired, -> (limit) { expired_before(Time.current).limit(limit) } end def each_blob(&blk) diff --git a/app/models/concerns/each_batch.rb b/app/models/concerns/each_batch.rb index af5f4e30d06..a59f00d73ec 100644 --- a/app/models/concerns/each_batch.rb +++ b/app/models/concerns/each_batch.rb @@ -47,7 +47,7 @@ module EachBatch # order_hint does not affect the search results. For example, # `ORDER BY id ASC, updated_at ASC` means the same thing as `ORDER # BY id ASC`. - def each_batch(of: 1000, column: primary_key, order_hint: nil) + def each_batch(of: 1000, column: primary_key, order: :asc, order_hint: nil) unless column raise ArgumentError, 'the column: argument must be set to a column name to use for ordering rows' @@ -55,7 +55,7 @@ module EachBatch start = except(:select) .select(column) - .reorder(column => :asc) + .reorder(column => order) start = start.order(order_hint) if order_hint start = start.take @@ -66,10 +66,12 @@ module EachBatch arel_table = self.arel_table 1.step do |index| + start_cond = arel_table[column].gteq(start_id) + start_cond = arel_table[column].lteq(start_id) if order == :desc stop = except(:select) .select(column) - .where(arel_table[column].gteq(start_id)) - .reorder(column => :asc) + .where(start_cond) + .reorder(column => order) stop = stop.order(order_hint) if order_hint stop = stop @@ -77,12 +79,14 @@ module EachBatch .limit(1) .take - relation = where(arel_table[column].gteq(start_id)) + relation = where(start_cond) if stop stop_id = stop[column] start_id = stop_id - relation = relation.where(arel_table[column].lt(stop_id)) + stop_cond = arel_table[column].lt(stop_id) + stop_cond = arel_table[column].gt(stop_id) if order == :desc + relation = relation.where(stop_cond) end # Any ORDER BYs are useless for this relation and can lead to less diff --git a/app/models/concerns/enums/ci/commit_status.rb b/app/models/concerns/enums/ci/commit_status.rb new file mode 100644 index 00000000000..48b4a402974 --- /dev/null +++ b/app/models/concerns/enums/ci/commit_status.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true +module Enums + module Ci + module CommitStatus + # Returns the Hash to use for creating the `failure_reason` enum for + # `CommitStatus`. + def self.failure_reasons + { + unknown_failure: nil, + script_failure: 1, + api_failure: 2, + stuck_or_timeout_failure: 3, + runner_system_failure: 4, + missing_dependency_failure: 5, + runner_unsupported: 6, + stale_schedule: 7, + job_execution_timeout: 8, + archived_failure: 9, + unmet_prerequisites: 10, + scheduler_failure: 11, + data_integrity_failure: 12, + forward_deployment_failure: 13, + insufficient_bridge_permissions: 1_001, + downstream_bridge_project_not_found: 1_002, + invalid_bridge_trigger: 1_003, + bridge_pipeline_is_child_pipeline: 1_006, # not used anymore, but cannot be deleted because of old data + downstream_pipeline_creation_failed: 1_007, + secrets_provider_not_found: 1_008, + reached_max_descendant_pipelines_depth: 1_009 + } + end + end + end +end + +Enums::Ci::CommitStatus.prepend_if_ee('EE::Enums::Ci::CommitStatus') diff --git a/app/models/concerns/enums/commit_status.rb b/app/models/concerns/enums/commit_status.rb deleted file mode 100644 index faeed7276ab..00000000000 --- a/app/models/concerns/enums/commit_status.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -module Enums - module CommitStatus - # Returns the Hash to use for creating the `failure_reason` enum for - # `CommitStatus`. - def self.failure_reasons - { - unknown_failure: nil, - script_failure: 1, - api_failure: 2, - stuck_or_timeout_failure: 3, - runner_system_failure: 4, - missing_dependency_failure: 5, - runner_unsupported: 6, - stale_schedule: 7, - job_execution_timeout: 8, - archived_failure: 9, - unmet_prerequisites: 10, - scheduler_failure: 11, - data_integrity_failure: 12, - forward_deployment_failure: 13, - insufficient_bridge_permissions: 1_001, - downstream_bridge_project_not_found: 1_002, - invalid_bridge_trigger: 1_003, - bridge_pipeline_is_child_pipeline: 1_006, # not used anymore, but cannot be deleted because of old data - downstream_pipeline_creation_failed: 1_007, - secrets_provider_not_found: 1_008, - reached_max_descendant_pipelines_depth: 1_009 - } - end - end -end - -Enums::CommitStatus.prepend_if_ee('EE::Enums::CommitStatus') diff --git a/app/models/concerns/enums/vulnerability.rb b/app/models/concerns/enums/vulnerability.rb new file mode 100644 index 00000000000..4b2e9e9e0b2 --- /dev/null +++ b/app/models/concerns/enums/vulnerability.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Enums + module Vulnerability + CONFIDENCE_LEVELS = { + # undefined: 0, no longer applicable + ignore: 1, + unknown: 2, + experimental: 3, + low: 4, + medium: 5, + high: 6, + confirmed: 7 + }.with_indifferent_access.freeze + + REPORT_TYPES = { + sast: 0, + secret_detection: 4 + }.with_indifferent_access.freeze + + SEVERITY_LEVELS = { + # undefined: 0, no longer applicable + info: 1, + unknown: 2, + # experimental: 3, formerly used by confidence, no longer applicable + low: 4, + medium: 5, + high: 6, + critical: 7 + }.with_indifferent_access.freeze + + def self.confidence_levels + CONFIDENCE_LEVELS + end + + def self.report_types + REPORT_TYPES + end + + def self.severity_levels + SEVERITY_LEVELS + end + end +end + +Enums::Vulnerability.prepend_if_ee('EE::Enums::Vulnerability') diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index c3a394c1ca5..83ff5b16efe 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -196,6 +196,10 @@ module Issuable is_a?(Issue) end + def supports_assignee? + false + end + def severity return IssuableSeverity::DEFAULT unless supports_severity? @@ -216,6 +220,10 @@ module Issuable end class_methods do + def participant_includes + [:assignees, :author, { notes: [:author, :award_emoji] }] + end + # Searches for records with a matching title. # # This method uses ILIKE on PostgreSQL. @@ -344,12 +352,15 @@ module Issuable # # Returns an array of arel columns def grouping_columns(sort) + sort = sort.to_s grouping_columns = [arel_table[:id]] if %w(milestone_due_desc milestone_due_asc milestone).include?(sort) milestone_table = Milestone.arel_table grouping_columns << milestone_table[:id] grouping_columns << milestone_table[:due_date] + elsif %w(merged_at_desc merged_at_asc).include?(sort) + grouping_columns << MergeRequest::Metrics.arel_table[:merged_at] end grouping_columns diff --git a/app/models/concerns/issue_available_features.rb b/app/models/concerns/issue_available_features.rb index 886db133a94..a5ffa959174 100644 --- a/app/models/concerns/issue_available_features.rb +++ b/app/models/concerns/issue_available_features.rb @@ -9,7 +9,10 @@ module IssueAvailableFeatures class_methods do # EE only features are listed on EE::IssueAvailableFeatures def available_features_for_issue_types - {}.with_indifferent_access + { + assignee: %w(issue incident), + confidentiality: %(issue incident) + }.with_indifferent_access end end diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb index b1698bc2ee3..ccb334343ff 100644 --- a/app/models/concerns/milestoneable.rb +++ b/app/models/concerns/milestoneable.rb @@ -51,7 +51,7 @@ module Milestoneable # Overridden on EE module # def supports_milestone? - respond_to?(:milestone_id) && !incident? + respond_to?(:milestone_id) end end diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index 2dbe9360d42..f3cc68e4b85 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -19,6 +19,11 @@ module Noteable def resolvable_types %w(MergeRequest DesignManagement::Design) end + + # `Noteable` class names that support creating/forwarding individual notes. + def email_creatable_types + %w(Issue) + end end # The timestamp of the note (e.g. the :created_at or :updated_at attribute if provided via @@ -55,6 +60,10 @@ module Noteable supports_discussions? && self.class.replyable_types.include?(base_class_name) end + def supports_creating_notes_by_email? + self.class.email_creatable_types.include?(base_class_name) + end + def supports_suggestion? false end @@ -158,6 +167,18 @@ module Noteable def after_note_destroyed(_note) # no-op end + + # Email address that an authorized user can send/forward an email to be added directly + # to an issue or merge request. + # example: incoming+h5bp-html5-boilerplate-8-1234567890abcdef123456789-issue-34@localhost.com + def creatable_note_email_address(author) + return unless supports_creating_notes_by_email? + + project_email = project.new_issuable_address(author, self.class.name.underscore) + return unless project_email + + project_email.sub('@', "-#{iid}@") + end end Noteable.extend(Noteable::ClassMethods) diff --git a/app/models/concerns/packages/debian/architecture.rb b/app/models/concerns/packages/debian/architecture.rb new file mode 100644 index 00000000000..4aa633e0357 --- /dev/null +++ b/app/models/concerns/packages/debian/architecture.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Packages + module Debian + module Architecture + extend ActiveSupport::Concern + + included do + belongs_to :distribution, class_name: "Packages::Debian::#{container_type.capitalize}Distribution", inverse_of: :architectures + + validates :distribution, + presence: true + + validates :name, + presence: true, + length: { maximum: 255 }, + uniqueness: { scope: %i[distribution_id] }, + format: { with: Gitlab::Regex.debian_architecture_regex } + + scope :with_distribution, ->(distribution) { where(distribution: distribution) } + scope :with_name, ->(name) { where(name: name) } + end + end + end +end diff --git a/app/models/concerns/packages/debian/distribution.rb b/app/models/concerns/packages/debian/distribution.rb new file mode 100644 index 00000000000..285d293c9ee --- /dev/null +++ b/app/models/concerns/packages/debian/distribution.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +module Packages + module Debian + module Distribution + extend ActiveSupport::Concern + + included do + include FileStoreMounter + + def self.container_foreign_key + "#{container_type}_id".to_sym + end + + alias_attribute :container, container_type + alias_attribute :container_id, "#{container_type}_id" + + belongs_to container_type + belongs_to :creator, class_name: 'User' + + has_many :architectures, + class_name: "Packages::Debian::#{container_type.capitalize}Architecture", + foreign_key: :distribution_id, + inverse_of: :distribution + + validates :codename, + presence: true, + uniqueness: { scope: [container_foreign_key] }, + format: { with: Gitlab::Regex.debian_distribution_regex } + + validates :suite, + allow_nil: true, + format: { with: Gitlab::Regex.debian_distribution_regex } + validates :suite, + uniqueness: { scope: [container_foreign_key] }, + if: :suite + + validate :unique_codename_and_suite + + validates :origin, + allow_nil: true, + format: { with: Gitlab::Regex.debian_distribution_regex } + + validates :label, + allow_nil: true, + format: { with: Gitlab::Regex.debian_distribution_regex } + + validates :version, + allow_nil: true, + format: { with: Gitlab::Regex.debian_version_regex } + + # The Valid-Until field is a security measure to prevent malicious attackers to + # serve an outdated repository, with vulnerable packages + # (keeping in mind that most Debian repository are not using TLS but use GPG + # signatures instead). + # A minimum of 24 hours is simply to avoid generating indices too often + # (which generates load). + # Official Debian repositories are generated 4 times a day, and valid for 7 days. + # Full ref: https://wiki.debian.org/DebianRepository/Format#Date.2C_Valid-Until + validates :valid_time_duration_seconds, + allow_nil: true, + numericality: { greater_than_or_equal_to: 24.hours.to_i } + + validates container_type, presence: true + validates :file_store, presence: true + + validates :file_signature, absence: true + validates :signing_keys, absence: true + + scope :with_container, ->(subject) { where(container_type => subject) } + scope :with_codename, ->(codename) { where(codename: codename) } + scope :with_suite, ->(suite) { where(suite: suite) } + scope :with_codename_or_suite, ->(codename_or_suite) { with_codename(codename_or_suite).or(with_suite(codename_or_suite)) } + + attr_encrypted :signing_keys, + mode: :per_attribute_iv, + key: Settings.attr_encrypted_db_key_base_truncated, + algorithm: 'aes-256-gcm', + encode: false, + encode_iv: false + + mount_file_store_uploader Packages::Debian::DistributionReleaseFileUploader + + def needs_update? + !file.exists? || time_duration_expired? + end + + private + + def time_duration_expired? + return false unless valid_time_duration_seconds.present? + + updated_at + valid_time_duration_seconds.seconds + 6.hours < Time.current + end + + def unique_codename_and_suite + errors.add(:codename, _('has already been taken as Suite')) if codename_exists_as_suite? + errors.add(:suite, _('has already been taken as Codename')) if suite_exists_as_codename? + end + + def codename_exists_as_suite? + return false unless codename.present? + + self.class.with_container(container).with_suite(codename).exists? + end + + def suite_exists_as_codename? + return false unless suite.present? + + self.class.with_container(container).with_codename(suite).exists? + end + end + end + end +end diff --git a/app/models/concerns/repositories/can_housekeep_repository.rb b/app/models/concerns/repositories/can_housekeep_repository.rb new file mode 100644 index 00000000000..2b79851a07c --- /dev/null +++ b/app/models/concerns/repositories/can_housekeep_repository.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Repositories + module CanHousekeepRepository + extend ActiveSupport::Concern + + def pushes_since_gc + Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i } + end + + def increment_pushes_since_gc + Gitlab::Redis::SharedState.with { |redis| redis.incr(pushes_since_gc_redis_shared_state_key) } + end + + def reset_pushes_since_gc + Gitlab::Redis::SharedState.with { |redis| redis.del(pushes_since_gc_redis_shared_state_key) } + end + + private + + def pushes_since_gc_redis_shared_state_key + "#{self.class.name.underscore.pluralize}/#{id}/pushes_since_gc" + end + end +end diff --git a/app/models/cycle_analytics/level_base.rb b/app/models/cycle_analytics/level_base.rb deleted file mode 100644 index 901636a7263..00000000000 --- a/app/models/cycle_analytics/level_base.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -module CycleAnalytics - module LevelBase - STAGES = %i[issue plan code test review staging].freeze - - # This is a temporary adapter class which makes the new value stream (cycle analytics) - # backend compatible with the old implementation. - class StageAdapter - def initialize(stage, options) - @stage = stage - @options = options - end - - # rubocop: disable CodeReuse/Presenter - def as_json(serializer: AnalyticsStageSerializer) - presenter = Analytics::CycleAnalytics::StagePresenter.new(stage) - - serializer.new.represent(OpenStruct.new( - title: presenter.title, - description: presenter.description, - legend: presenter.legend, - name: stage.name, - project_median: median, - group_median: median - )) - end - # rubocop: enable CodeReuse/Presenter - - def events - data_collector.records_fetcher.serialized_records - end - - def median - data_collector.median.seconds - end - - alias_method :project_median, :median - alias_method :group_median, :median - - private - - attr_reader :stage, :options - - def data_collector - @data_collector ||= Gitlab::Analytics::CycleAnalytics::DataCollector.new(stage: stage, params: options) - end - end - - def all_medians_by_stage - STAGES.each_with_object({}) do |stage_name, medians_per_stage| - medians_per_stage[stage_name] = self[stage_name].median - end - end - - def stats - @stats ||= STAGES.map do |stage_name| - self[stage_name].as_json - end - end - - def [](stage_name) - if Feature.enabled?(:new_project_level_vsa_backend, resource_parent, default_enabled: true) - StageAdapter.new(build_stage(stage_name), options) - else - Gitlab::CycleAnalytics::Stage[stage_name].new(options: options) - end - end - - def stage_params_by_name(name) - Gitlab::Analytics::CycleAnalytics::DefaultStages.find_by_name!(name) - end - end -end diff --git a/app/models/cycle_analytics/project_level.rb b/app/models/cycle_analytics/project_level.rb index 26cdcc0db4b..5bd07b3f6c3 100644 --- a/app/models/cycle_analytics/project_level.rb +++ b/app/models/cycle_analytics/project_level.rb @@ -2,7 +2,6 @@ module CycleAnalytics class ProjectLevel - include LevelBase attr_reader :project, :options def initialize(project, options:) @@ -21,13 +20,29 @@ module CycleAnalytics Gitlab::CycleAnalytics::Permissions.get(user: user, project: project) end + def stats + @stats ||= default_stage_names.map do |stage_name| + self[stage_name].as_json + end + end + + def [](stage_name) + CycleAnalytics::ProjectLevelStageAdapter.new(build_stage(stage_name), options) + end + + private + def build_stage(stage_name) stage_params = stage_params_by_name(stage_name).merge(project: project) Analytics::CycleAnalytics::ProjectStage.new(stage_params) end - def resource_parent - project + def stage_params_by_name(name) + Gitlab::Analytics::CycleAnalytics::DefaultStages.find_by_name!(name) + end + + def default_stage_names + Gitlab::Analytics::CycleAnalytics::DefaultStages.symbolized_stage_names end end end diff --git a/app/models/cycle_analytics/project_level_stage_adapter.rb b/app/models/cycle_analytics/project_level_stage_adapter.rb new file mode 100644 index 00000000000..dd4afa9b809 --- /dev/null +++ b/app/models/cycle_analytics/project_level_stage_adapter.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# This adapter class makes the new value stream (cycle analytics) backend +# compatible with the old value stream controller actions. +module CycleAnalytics + class ProjectLevelStageAdapter + def initialize(stage, options) + @stage = stage + @options = options + end + + # rubocop: disable CodeReuse/Presenter + def as_json(serializer: AnalyticsStageSerializer) + presenter = Analytics::CycleAnalytics::StagePresenter.new(stage) + + serializer.new.represent(OpenStruct.new( + title: presenter.title, + description: presenter.description, + legend: presenter.legend, + name: stage.name, + project_median: median + )) + end + # rubocop: enable CodeReuse/Presenter + + def events + data_collector.records_fetcher.serialized_records + end + + def median + data_collector.median.seconds + end + + alias_method :project_median, :median + + private + + attr_reader :stage, :options + + def data_collector + @data_collector ||= Gitlab::Analytics::CycleAnalytics::DataCollector.new(stage: stage, params: options) + end + end +end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index b93b714ec8b..6f40466394a 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -109,6 +109,23 @@ class Deployment < ApplicationRecord Deployments::ExecuteHooksWorker.perform_async(id) end end + + after_transition any => any - [:skipped] do |deployment, transition| + next if transition.loopback? + next unless Feature.enabled?(:jira_sync_deployments, deployment.project) + + deployment.run_after_commit do + ::JiraConnect::SyncDeploymentsWorker.perform_async(id) + end + end + end + + after_create unless: :importing? do |deployment| + next unless Feature.enabled?(:jira_sync_deployments, deployment.project) + + run_after_commit do + ::JiraConnect::SyncDeploymentsWorker.perform_async(deployment.id) + end end enum status: { diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index 944a64f5419..c8a0773cc5b 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -111,6 +111,10 @@ class DiffNote < Note super.merge(suggestions_filter_enabled: true) end + def multiline? + position&.multiline? + end + private def enqueue_diff_file_creation_job diff --git a/app/models/experiment.rb b/app/models/experiment.rb index a4cacab25ee..7dbc95f617a 100644 --- a/app/models/experiment.rb +++ b/app/models/experiment.rb @@ -16,7 +16,9 @@ class Experiment < ApplicationRecord # Create or update the recorded experiment_user row for the user in this experiment. def record_user_and_group(user, group_type, context = {}) - experiment_users.find_or_initialize_by(user: user).update!(group_type: group_type, context: context) + experiment_user = experiment_users.find_or_initialize_by(user: user) + merged_context = experiment_user.context.deep_merge(context.deep_stringify_keys) + experiment_user.update!(group_type: group_type, context: merged_context) end def record_conversion_event_for_user(user) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 995baf8565c..ca6857a14b6 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -127,3 +127,5 @@ class GpgKey < ApplicationRecord end end end + +GpgKey.prepend_if_ee('EE::GpgKey') diff --git a/app/models/group.rb b/app/models/group.rb index 739135e82dd..903d0154969 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -75,6 +75,9 @@ class Group < Namespace has_many :dependency_proxy_blobs, class_name: 'DependencyProxy::Blob' has_many :dependency_proxy_manifests, class_name: 'DependencyProxy::Manifest' + # debian_distributions must be destroyed by ruby code in order to properly remove carrierwave uploads + has_many :debian_distributions, class_name: 'Packages::Debian::GroupDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + accepts_nested_attributes_for :variables, allow_destroy: true validate :visibility_level_allowed_by_projects diff --git a/app/models/issue.rb b/app/models/issue.rb index 253f4465cd9..5da9f67f6ef 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -434,6 +434,10 @@ class Issue < ApplicationRecord moved_to || duplicated_to end + def supports_assignee? + issue_type_supports?(:assignee) + end + private def ensure_metrics diff --git a/app/models/list.rb b/app/models/list.rb index 1df565c83e6..49834af3dfb 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class List < ApplicationRecord + include Boards::Listable include Importable belongs_to :board @@ -10,30 +11,13 @@ class List < ApplicationRecord enum list_type: { backlog: 0, label: 1, closed: 2, assignee: 3, milestone: 4, iteration: 5 } validates :board, :list_type, presence: true, unless: :importing? - validates :label, :position, presence: true, if: :label? validates :label_id, uniqueness: { scope: :board_id }, if: :label? - validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :movable? - - before_destroy :can_be_destroyed - - scope :destroyable, -> { where(list_type: list_types.slice(*destroyable_types).values) } - scope :movable, -> { where(list_type: list_types.slice(*movable_types).values) } scope :preload_associated_models, -> { preload(:board, label: :priorities) } - scope :ordered, -> { order(:list_type, :position) } - alias_method :preferences, :list_user_preferences class << self - def destroyable_types - [:label] - end - - def movable_types - [:label] - end - def preload_preferences_for_user(lists, user) return unless user @@ -60,18 +44,6 @@ class List < ApplicationRecord preferences_for(user).update(preferences) end - def destroyable? - self.class.destroyable_types.include?(list_type&.to_sym) - end - - def movable? - self.class.movable_types.include?(list_type&.to_sym) - end - - def title - label? ? label.name : list_type.humanize - end - def collapsed?(user) preferences = preferences_for(user) @@ -95,12 +67,6 @@ class List < ApplicationRecord end end end - - private - - def can_be_destroyed - throw(:abort) unless destroyable? # rubocop:disable Cop/BanCatchThrow - end end List.prepend_if_ee('::EE::List') diff --git a/app/models/member.rb b/app/models/member.rb index 687830f5267..2e79b50d6b7 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -13,6 +13,8 @@ class Member < ApplicationRecord include FromUnion include UpdateHighestRole + AVATAR_SIZE = 40 + attr_accessor :raw_invite_token belongs_to :created_by, class_name: "User" diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 2bbcdbbe5ce..c30f6dc81ee 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -62,7 +62,9 @@ class GroupMember < Member end def post_create_hook - run_after_commit_or_now { notification_service.new_group_member(self) } + if send_welcome_email? + run_after_commit_or_now { notification_service.new_group_member(self) } + end super end @@ -72,6 +74,10 @@ class GroupMember < Member run_after_commit { notification_service.update_group_member(self) } end + if saved_change_to_expires_at? + run_after_commit { notification_service.updated_group_member_expiration(self) } + end + super end @@ -87,6 +93,10 @@ class GroupMember < Member super end + + def send_welcome_email? + true + end end GroupMember.prepend_if_ee('EE::GroupMember') diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 043f07cf9f3..64b8223a1f0 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -261,6 +261,19 @@ class MergeRequest < ApplicationRecord scope :by_merge_commit_sha, -> (sha) do where(merge_commit_sha: sha) end + scope :by_squash_commit_sha, -> (sha) do + where(squash_commit_sha: sha) + end + scope :by_related_commit_sha, -> (sha) do + from_union( + [ + by_commit_sha(sha), + by_squash_commit_sha(sha), + by_merge_commit_sha(sha) + ], + remove_duplicates: false + ) + end scope :by_cherry_pick_sha, -> (sha) do joins(:notes).where(notes: { commit_id: sha }) end @@ -493,6 +506,10 @@ class MergeRequest < ApplicationRecord work_in_progress?(title) ? title : "Draft: #{title}" end + def self.participant_includes + [:reviewers, :award_emoji] + super + end + def committers @committers ||= commits.committers end @@ -1639,18 +1656,6 @@ class MergeRequest < ApplicationRecord !has_commits? end - def mergeable_with_quick_action?(current_user, autocomplete_precheck: false, last_diff_sha: nil) - return false unless can_be_merged_by?(current_user) - - return true if autocomplete_precheck - - return false unless mergeable?(skip_ci_check: true) - return false if actual_head_pipeline && !(actual_head_pipeline.success? || actual_head_pipeline.active?) - return false if last_diff_sha != diff_head_sha - - true - end - def pipeline_coverage_delta if base_pipeline&.coverage && head_pipeline&.coverage '%.2f' % (head_pipeline.coverage.to_f - base_pipeline.coverage.to_f) @@ -1762,6 +1767,10 @@ class MergeRequest < ApplicationRecord false end + def supports_assignee? + true + end + private def with_rebase_lock diff --git a/app/models/milestone.rb b/app/models/milestone.rb index c244150e7a3..aa4ddfede99 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -33,6 +33,7 @@ class Milestone < ApplicationRecord scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) } scope :reorder_by_due_date_asc, -> { reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) } scope :with_api_entity_associations, -> { preload(project: [:project_feature, :route, namespace: :route]) } + scope :order_by_dates_and_title, -> { order(due_date: :asc, start_date: :asc, title: :asc) } validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") } diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 238e8f70778..6f7b377ee52 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -28,7 +28,7 @@ class Namespace < ApplicationRecord has_many :runner_namespaces, inverse_of: :namespace, class_name: 'Ci::RunnerNamespace' has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner' - has_many :namespace_onboarding_actions + has_one :onboarding_progress # This should _not_ be `inverse_of: :namespace`, because that would also set # `user.namespace` when this user creates a group with themselves as `owner`. @@ -40,6 +40,7 @@ class Namespace < ApplicationRecord has_one :chat_team, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_one :root_storage_statistics, class_name: 'Namespace::RootStorageStatistics' has_one :aggregation_schedule, class_name: 'Namespace::AggregationSchedule' + has_one :package_setting_relation, inverse_of: :namespace, class_name: 'PackageSetting' validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :name, @@ -160,6 +161,10 @@ class Namespace < ApplicationRecord end end + def package_settings + package_setting_relation || build_package_setting_relation + end + def default_branch_protection super || Gitlab::CurrentSettings.default_branch_protection end @@ -438,6 +443,10 @@ class Namespace < ApplicationRecord end end + def root? + !has_parent? + end + private def all_projects_with_pages diff --git a/app/models/namespace/package_setting.rb b/app/models/namespace/package_setting.rb new file mode 100644 index 00000000000..a2064e020b3 --- /dev/null +++ b/app/models/namespace/package_setting.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class Namespace::PackageSetting < ApplicationRecord + self.primary_key = :namespace_id + self.table_name = 'namespace_package_settings' + + PackageSettingNotImplemented = Class.new(StandardError) + + PACKAGES_WITH_SETTINGS = %w[maven].freeze + + belongs_to :namespace, inverse_of: :package_setting_relation + + validates :namespace, presence: true + validates :maven_duplicates_allowed, inclusion: { in: [true, false] } + validates :maven_duplicate_exception_regex, untrusted_regexp: true, length: { maximum: 255 } + + class << self + def duplicates_allowed?(package) + return true unless package + raise PackageSettingNotImplemented unless PACKAGES_WITH_SETTINGS.include?(package.package_type) + + duplicates_allowed = package.package_settings["#{package.package_type}_duplicates_allowed"] + regex = ::Gitlab::UntrustedRegexp.new("\\A#{package.package_settings["#{package.package_type}_duplicate_exception_regex"]}\\z") + + duplicates_allowed || regex.match?(package.name) + end + end +end diff --git a/app/models/namespace/root_storage_statistics.rb b/app/models/namespace/root_storage_statistics.rb index a3df82998c4..90aeee7a4f1 100644 --- a/app/models/namespace/root_storage_statistics.rb +++ b/app/models/namespace/root_storage_statistics.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Namespace::RootStorageStatistics < ApplicationRecord - SNIPPETS_SIZE_STAT_NAME = 'snippets_size'.freeze + SNIPPETS_SIZE_STAT_NAME = 'snippets_size' STATISTICS_ATTRIBUTES = %W( storage_size repository_size diff --git a/app/models/namespace_onboarding_action.rb b/app/models/namespace_onboarding_action.rb deleted file mode 100644 index 43dd872673c..00000000000 --- a/app/models/namespace_onboarding_action.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -class NamespaceOnboardingAction < ApplicationRecord - belongs_to :namespace, optional: false - - validates :action, presence: true - - ACTIONS = { - subscription_created: 1, - git_write: 2, - merge_request_created: 3, - git_read: 4, - user_added: 6 - }.freeze - - enum action: ACTIONS - - class << self - def completed?(namespace, action) - where(namespace: namespace, action: action).exists? - end - - def create_action(namespace, action) - NamespaceOnboardingAction.safe_find_or_create_by(namespace: namespace, action: action) - end - end -end diff --git a/app/models/onboarding_progress.rb b/app/models/onboarding_progress.rb new file mode 100644 index 00000000000..419bbd595e9 --- /dev/null +++ b/app/models/onboarding_progress.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +class OnboardingProgress < ApplicationRecord + belongs_to :namespace, optional: false + + validate :namespace_is_root_namespace + + ACTIONS = [ + :git_pull, + :git_write, + :merge_request_created, + :pipeline_created, + :user_added, + :trial_started, + :subscription_created, + :required_mr_approvals_enabled, + :code_owners_enabled, + :scoped_label_created, + :security_scan_enabled, + :issue_auto_closed, + :repository_imported, + :repository_mirrored + ].freeze + + class << self + def onboard(namespace) + return unless root_namespace?(namespace) + + safe_find_or_create_by(namespace: namespace) + end + + def register(namespace, action) + return unless root_namespace?(namespace) && ACTIONS.include?(action) + + action_column = column_name(action) + onboarding_progress = find_by(namespace: namespace, action_column => nil) + onboarding_progress&.update!(action_column => Time.current) + end + + def completed?(namespace, action) + return unless root_namespace?(namespace) && ACTIONS.include?(action) + + action_column = column_name(action) + where(namespace: namespace).where.not(action_column => nil).exists? + end + + private + + def column_name(action) + :"#{action}_at" + end + + def root_namespace?(namespace) + namespace && namespace.root? + end + end + + private + + def namespace_is_root_namespace + return unless namespace + + errors.add(:namespace, _('must be a root namespace')) if namespace.has_parent? + end +end diff --git a/app/models/packages/conan/file_metadatum.rb b/app/models/packages/conan/file_metadatum.rb index de54580e948..64ae5cd88a5 100644 --- a/app/models/packages/conan/file_metadatum.rb +++ b/app/models/packages/conan/file_metadatum.rb @@ -3,8 +3,8 @@ class Packages::Conan::FileMetadatum < ApplicationRecord belongs_to :package_file, inverse_of: :conan_file_metadatum - DEFAULT_PACKAGE_REVISION = '0'.freeze - DEFAULT_RECIPE_REVISION = '0'.freeze + DEFAULT_PACKAGE_REVISION = '0' + DEFAULT_RECIPE_REVISION = '0' validates :package_file, presence: true diff --git a/app/models/packages/debian.rb b/app/models/packages/debian.rb new file mode 100644 index 00000000000..f7f7f9f95e9 --- /dev/null +++ b/app/models/packages/debian.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Packages + module Debian + def self.table_name_prefix + 'packages_debian_' + end + end +end diff --git a/app/models/packages/debian/file_metadatum.rb b/app/models/packages/debian/file_metadatum.rb new file mode 100644 index 00000000000..7c9f4f5f3f1 --- /dev/null +++ b/app/models/packages/debian/file_metadatum.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +class Packages::Debian::FileMetadatum < ApplicationRecord + belongs_to :package_file, inverse_of: :debian_file_metadatum + + validates :package_file, presence: true + validate :valid_debian_package_type + + enum file_type: { + unknown: 1, source: 2, dsc: 3, deb: 4, udeb: 5, buildinfo: 6, changes: 7 + } + + validates :file_type, presence: true + validates :file_type, inclusion: { in: %w[unknown] }, if: -> { package_file&.package&.debian_incoming? } + validates :file_type, + inclusion: { in: %w[source dsc deb udeb buildinfo changes] }, + if: -> { package_file&.package&.debian_package? } + + validates :component, + presence: true, + format: { with: Gitlab::Regex.debian_component_regex }, + if: :requires_component? + validates :component, absence: true, unless: :requires_component? + + validates :architecture, + presence: true, + format: { with: Gitlab::Regex.debian_architecture_regex }, + if: :requires_architecture? + validates :architecture, absence: true, unless: :requires_architecture? + + validates :fields, + presence: true, + json_schema: { filename: "debian_fields" }, + if: :requires_fields? + validates :fields, absence: true, unless: :requires_fields? + + private + + def valid_debian_package_type + return if package_file&.package&.debian? + + errors.add(:package_file, _('Package type must be Debian')) + end + + def requires_architecture? + deb? || udeb? + end + + def requires_component? + source? || dsc? || requires_architecture? || buildinfo? + end + + def requires_fields? + dsc? || requires_architecture? || buildinfo? || changes? + end +end diff --git a/app/models/packages/debian/group_architecture.rb b/app/models/packages/debian/group_architecture.rb new file mode 100644 index 00000000000..570f6accd3c --- /dev/null +++ b/app/models/packages/debian/group_architecture.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Packages::Debian::GroupArchitecture < ApplicationRecord + def self.container_type + :group + end + + include Packages::Debian::Architecture +end diff --git a/app/models/packages/debian/group_distribution.rb b/app/models/packages/debian/group_distribution.rb new file mode 100644 index 00000000000..eea7acacc96 --- /dev/null +++ b/app/models/packages/debian/group_distribution.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Packages::Debian::GroupDistribution < ApplicationRecord + def self.container_type + :group + end + + include Packages::Debian::Distribution +end diff --git a/app/models/packages/debian/project_architecture.rb b/app/models/packages/debian/project_architecture.rb new file mode 100644 index 00000000000..44a38dfaf44 --- /dev/null +++ b/app/models/packages/debian/project_architecture.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Packages::Debian::ProjectArchitecture < ApplicationRecord + def self.container_type + :project + end + + include Packages::Debian::Architecture +end diff --git a/app/models/packages/debian/project_distribution.rb b/app/models/packages/debian/project_distribution.rb new file mode 100644 index 00000000000..a73c12d172d --- /dev/null +++ b/app/models/packages/debian/project_distribution.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Packages::Debian::ProjectDistribution < ApplicationRecord + def self.container_type + :project + end + + include Packages::Debian::Distribution +end diff --git a/app/models/packages/dependency.rb b/app/models/packages/dependency.rb index 51b80934827..a32c3c05bb3 100644 --- a/app/models/packages/dependency.rb +++ b/app/models/packages/dependency.rb @@ -6,7 +6,7 @@ class Packages::Dependency < ApplicationRecord validates :name, uniqueness: { scope: :version_pattern } - NAME_VERSION_PATTERN_TUPLE_MATCHING = '(name, version_pattern) = (?, ?)'.freeze + NAME_VERSION_PATTERN_TUPLE_MATCHING = '(name, version_pattern) = (?, ?)' MAX_STRING_LENGTH = 255.freeze MAX_CHUNKED_QUERIES_COUNT = 10.freeze diff --git a/app/models/packages/event.rb b/app/models/packages/event.rb index 13da82d16d3..98c9d5246db 100644 --- a/app/models/packages/event.rb +++ b/app/models/packages/event.rb @@ -6,6 +6,8 @@ class Packages::Event < ApplicationRecord UNIQUE_EVENTS_ALLOWED = %i[push_package delete_package pull_package].freeze EVENT_SCOPES = ::Packages::Package.package_types.merge(container: 1000, tag: 1001).freeze + EVENT_PREFIX = "i_package" + enum event_scope: EVENT_SCOPES enum event_type: { @@ -24,13 +26,6 @@ class Packages::Event < ApplicationRecord enum originator_type: { user: 0, deploy_token: 1, guest: 2 } - def self.allowed_event_name(event_scope, event_type, originator) - return unless event_allowed?(event_type) - - # remove `package` from the event name to avoid issues with HLLRedisCounter class parsing - "i_package_#{event_scope}_#{originator}_#{event_type.gsub(/_packages?/, "")}" - end - # Remove some of the events, for now, so we don't hammer Redis too hard. # See: https://gitlab.com/gitlab-org/gitlab/-/issues/280770 def self.event_allowed?(event_type) @@ -38,4 +33,23 @@ class Packages::Event < ApplicationRecord false end + + # counter names for unique user tracking (for MAU) + def self.unique_counters_for(event_scope, event_type, originator_type) + return [] unless event_allowed?(event_type) + return [] if originator_type.to_s == 'guest' + + ["#{EVENT_PREFIX}_#{event_scope}_#{originator_type}"] + end + + # total counter names for tracking number of events + def self.counters_for(event_scope, event_type, originator_type) + return [] unless event_allowed?(event_type) + + [ + "#{EVENT_PREFIX}_#{event_type}", + "#{EVENT_PREFIX}_#{event_type}_by_#{originator_type}", + "#{EVENT_PREFIX}_#{event_scope}_#{event_type}" + ] + end end diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb index 10c98f03804..2067a800ad5 100644 --- a/app/models/packages/package.rb +++ b/app/models/packages/package.rb @@ -112,6 +112,7 @@ class Packages::Package < ApplicationRecord scope :order_project_name_desc, -> { joins(:project).reorder('projects.name DESC') } scope :order_project_path, -> { joins(:project).reorder('projects.path ASC, id ASC') } scope :order_project_path_desc, -> { joins(:project).reorder('projects.path DESC, id DESC') } + scope :order_by_package_file, -> { joins(:package_files).order('packages_package_files.created_at ASC') } def self.for_projects(projects) return none unless projects.any? @@ -199,6 +200,12 @@ class Packages::Package < ApplicationRecord debian? && !version.nil? end + def package_settings + strong_memoize(:package_settings) do + project.namespace.package_settings + end + end + private def composer_tag_version? diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb index e8d1dd1e8c4..389edaea392 100644 --- a/app/models/packages/package_file.rb +++ b/app/models/packages/package_file.rb @@ -5,14 +5,17 @@ class Packages::PackageFile < ApplicationRecord delegate :project, :project_id, to: :package delegate :conan_file_type, to: :conan_file_metadatum + delegate :file_type, :architecture, :fields, to: :debian_file_metadatum, prefix: :debian belongs_to :package has_one :conan_file_metadatum, inverse_of: :package_file, class_name: 'Packages::Conan::FileMetadatum' has_many :package_file_build_infos, inverse_of: :package_file, class_name: 'Packages::PackageFileBuildInfo' has_many :pipelines, through: :package_file_build_infos + has_one :debian_file_metadatum, inverse_of: :package_file, class_name: 'Packages::Debian::FileMetadatum' accepts_nested_attributes_for :conan_file_metadatum + accepts_nested_attributes_for :debian_file_metadatum validates :package, presence: true validates :file, presence: true @@ -25,12 +28,18 @@ class Packages::PackageFile < ApplicationRecord scope :with_file_name_like, ->(file_name) { where(arel_table[:file_name].matches(file_name)) } scope :with_files_stored_locally, -> { where(file_store: ::Packages::PackageFileUploader::Store::LOCAL) } scope :preload_conan_file_metadata, -> { preload(:conan_file_metadatum) } + scope :preload_debian_file_metadata, -> { preload(:debian_file_metadatum) } scope :with_conan_file_type, ->(file_type) do joins(:conan_file_metadatum) .where(packages_conan_file_metadata: { conan_file_type: ::Packages::Conan::FileMetadatum.conan_file_types[file_type] }) end + scope :with_debian_file_type, ->(file_type) do + joins(:debian_file_metadatum) + .where(packages_debian_file_metadata: { debian_file_type: ::Packages::Debian::FileMetadatum.debian_file_types[file_type] }) + end + scope :with_conan_package_reference, ->(conan_package_reference) do joins(:conan_file_metadatum) .where(packages_conan_file_metadata: { conan_package_reference: conan_package_reference }) diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 4004ea9a662..4d60489e599 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -188,7 +188,7 @@ class PagesDomain < ApplicationRecord def user_provided_key=(key) self.key = key - self.certificate_source = 'user_provided' if key_changed? + self.certificate_source = 'user_provided' if attribute_changed?(:key) end def user_provided_certificate @@ -207,7 +207,7 @@ class PagesDomain < ApplicationRecord def gitlab_provided_key=(key) self.key = key - self.certificate_source = 'gitlab_provided' if key_changed? + self.certificate_source = 'gitlab_provided' if attribute_changed?(:key) end def pages_virtual_domain diff --git a/app/models/plan.rb b/app/models/plan.rb index b4091e0a755..6a7f32a5d5f 100644 --- a/app/models/plan.rb +++ b/app/models/plan.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Plan < ApplicationRecord - DEFAULT = 'default'.freeze + DEFAULT = 'default' has_one :limits, class_name: 'PlanLimits' diff --git a/app/models/project.rb b/app/models/project.rb index daa5605c2e0..ec790798806 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -34,6 +34,7 @@ class Project < ApplicationRecord include FromUnion include IgnorableColumns include Integration + include Repositories::CanHousekeepRepository include EachBatch extend Gitlab::Cache::RequestCache extend Gitlab::Utils::Override @@ -146,7 +147,6 @@ class Project < ApplicationRecord has_many :boards # Project services - has_one :alerts_service has_one :campfire_service has_one :datadog_service has_one :discord_service @@ -200,6 +200,8 @@ class Project < ApplicationRecord # Packages has_many :packages, class_name: 'Packages::Package' has_many :package_files, through: :packages, class_name: 'Packages::PackageFile' + # debian_distributions must be destroyed by ruby code in order to properly remove carrierwave uploads + has_many :debian_distributions, class_name: 'Packages::Debian::ProjectDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent @@ -408,6 +410,9 @@ class Project < ApplicationRecord delegate :dashboard_timezone, to: :metrics_setting, allow_nil: true, prefix: true delegate :default_git_depth, :default_git_depth=, to: :ci_cd_settings, prefix: :ci delegate :forward_deployment_enabled, :forward_deployment_enabled=, :forward_deployment_enabled?, to: :ci_cd_settings, prefix: :ci + delegate :keep_latest_artifact, :keep_latest_artifact=, :keep_latest_artifact?, to: :ci_cd_settings, prefix: :ci + delegate :restrict_user_defined_variables, :restrict_user_defined_variables=, :restrict_user_defined_variables?, + to: :ci_cd_settings delegate :actual_limits, :actual_plan_name, to: :namespace, allow_nil: true delegate :allow_merge_on_skipped_pipeline, :allow_merge_on_skipped_pipeline?, :allow_merge_on_skipped_pipeline=, :has_confluence?, :allow_editing_commit_messages?, @@ -831,6 +836,10 @@ class Project < ApplicationRecord webide_pipelines.running_or_pending.for_user(user) end + def latest_pipeline_locked + ci_keep_latest_artifact? ? :artifacts_locked : :unlocked + end + def autoclose_referenced_issues return true if super.nil? @@ -1331,19 +1340,11 @@ class Project < ApplicationRecord end def external_wiki - if has_external_wiki.nil? - cache_has_external_wiki - end + cache_has_external_wiki if has_external_wiki.nil? - if has_external_wiki - @external_wiki ||= services.external_wikis.first - else - nil - end - end + return unless has_external_wiki? - def cache_has_external_wiki - update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write? + @external_wiki ||= services.external_wikis.first end def find_or_initialize_services @@ -1355,9 +1356,9 @@ class Project < ApplicationRecord end def disabled_services - return ['datadog'] unless Feature.enabled?(:datadog_ci_integration, self) + return %w(datadog alerts) unless Feature.enabled?(:datadog_ci_integration, self) - [] + %w(alerts) end def find_or_initialize_service(name) @@ -1829,6 +1830,15 @@ class Project < ApplicationRecord ensure_pages_metadatum.update!(pages_deployment: deployment) end + def set_first_pages_deployment!(deployment) + ensure_pages_metadatum + + # where().update_all to perform update in the single transaction with check for null + ProjectPagesMetadatum + .where(project_id: id, pages_deployment_id: nil) + .update_all(pages_deployment_id: deployment.id) + end + def write_repository_config(gl_full_path: full_path) # We'd need to keep track of project full path otherwise directory tree # created with hashed storage enabled cannot be usefully imported using @@ -1980,6 +1990,7 @@ class Project < ApplicationRecord .append(key: 'CI_PROJECT_VISIBILITY', value: Gitlab::VisibilityLevel.string_level(visibility_level)) .append(key: 'CI_PROJECT_REPOSITORY_LANGUAGES', value: repository_languages.map(&:name).join(',').downcase) .append(key: 'CI_DEFAULT_BRANCH', value: default_branch) + .append(key: 'CI_PROJECT_CONFIG_PATH', value: ci_config_path_or_default) end def predefined_ci_server_variables @@ -2113,18 +2124,6 @@ class Project < ApplicationRecord (auto_devops || build_auto_devops)&.predefined_variables end - def pushes_since_gc - Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i } - end - - def increment_pushes_since_gc - Gitlab::Redis::SharedState.with { |redis| redis.incr(pushes_since_gc_redis_shared_state_key) } - end - - def reset_pushes_since_gc - Gitlab::Redis::SharedState.with { |redis| redis.del(pushes_since_gc_redis_shared_state_key) } - end - def route_map_for(commit_sha) @route_maps_by_commit ||= Hash.new do |h, sha| h[sha] = begin @@ -2430,10 +2429,6 @@ class Project < ApplicationRecord protected_branches.limit(limit) end - def alerts_service_activated? - alerts_service&.active? - end - def self_monitoring? Gitlab::CurrentSettings.self_monitoring_project_id == id end @@ -2486,16 +2481,12 @@ class Project < ApplicationRecord end def service_desk_custom_address - return unless service_desk_custom_address_enabled? + return unless Gitlab::ServiceDeskEmail.enabled? key = service_desk_setting&.project_key return unless key.present? - ::Gitlab::ServiceDeskEmail.address_for_key("#{full_path_slug}-#{key}") - end - - def service_desk_custom_address_enabled? - ::Gitlab::ServiceDeskEmail.enabled? && ::Feature.enabled?(:service_desk_custom_address, self, default_enabled: true) + Gitlab::ServiceDeskEmail.address_for_key("#{full_path_slug}-#{key}") end def root_namespace @@ -2633,10 +2624,6 @@ class Project < ApplicationRecord from && self != from end - def pushes_since_gc_redis_shared_state_key - "projects/#{id}/pushes_since_gc" - end - def update_project_statistics stats = statistics || build_statistics stats.update(namespace_id: namespace_id) @@ -2699,6 +2686,10 @@ class Project < ApplicationRecord objects.each_batch { |relation| out.concat(relation.pluck(:oid)) } end end + + def cache_has_external_wiki + update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write? + end end Project.prepend_if_ee('EE::Project') diff --git a/app/models/project_feature_usage.rb b/app/models/project_feature_usage.rb index b167c2e371b..4f445758653 100644 --- a/app/models/project_feature_usage.rb +++ b/app/models/project_feature_usage.rb @@ -3,8 +3,8 @@ class ProjectFeatureUsage < ApplicationRecord self.primary_key = :project_id - JIRA_DVCS_CLOUD_FIELD = 'jira_dvcs_cloud_last_sync_at'.freeze - JIRA_DVCS_SERVER_FIELD = 'jira_dvcs_server_last_sync_at'.freeze + JIRA_DVCS_CLOUD_FIELD = 'jira_dvcs_cloud_last_sync_at' + JIRA_DVCS_SERVER_FIELD = 'jira_dvcs_server_last_sync_at' belongs_to :project validates :project, presence: true diff --git a/app/models/project_pages_metadatum.rb b/app/models/project_pages_metadatum.rb index bd1919fe7ed..2bef0056732 100644 --- a/app/models/project_pages_metadatum.rb +++ b/app/models/project_pages_metadatum.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ProjectPagesMetadatum < ApplicationRecord + include EachBatch + self.primary_key = :project_id belongs_to :project, inverse_of: :pages_metadatum @@ -8,4 +10,5 @@ class ProjectPagesMetadatum < ApplicationRecord belongs_to :pages_deployment scope :deployed, -> { where(deployed: true) } + scope :only_on_legacy_storage, -> { deployed.where(pages_deployment: nil) } end diff --git a/app/models/project_services/alerts_service.rb b/app/models/project_services/alerts_service.rb index 5b7d149ace1..4afce0dfe95 100644 --- a/app/models/project_services/alerts_service.rb +++ b/app/models/project_services/alerts_service.rb @@ -1,54 +1,10 @@ # frozen_string_literal: true -require 'securerandom' - +# This service is scheduled for removal. All records must +# be deleted before the class can be removed. +# https://gitlab.com/groups/gitlab-org/-/epics/5056 class AlertsService < Service - has_one :data, class_name: 'AlertsServiceData', autosave: true, - inverse_of: :service, foreign_key: :service_id - - attribute :token, :string - delegate :token, :token=, :token_changed?, :token_was, to: :data - - validates :token, presence: true, if: :activated? - - before_validation :prevent_token_assignment - before_validation :ensure_token, if: :activated? - - after_save :update_http_integration - - def url - return if instance? || template? - - url_helpers.project_alerts_notify_url(project, format: :json) - end - - def json_fields - super + %w(token) - end - - def editable? - false - end - - def show_active_box? - false - end - - def can_test? - false - end - - def title - _('Alerts endpoint') - end - - def description - _('Authorize external services to send alerts to GitLab') - end - - def detailed_description - description - end + before_save :prevent_save def self.to_param 'alerts' @@ -58,35 +14,15 @@ class AlertsService < Service %w() end - def data - super || build_data - end - private - def prevent_token_assignment - self.token = token_was if token.present? && token_changed? - end - - def ensure_token - self.token = generate_token if token.blank? - end - - def generate_token - SecureRandom.hex - end - - def url_helpers - Gitlab::Routing.url_helpers - end - - def update_http_integration - return unless project_id && type == 'AlertsService' + def prevent_save + errors.add(:base, _('Alerts endpoint is deprecated and should not be created or modified. Use HTTP Integrations instead.')) + log_error('Prevented attempt to save or update deprecated AlertsService') - AlertManagement::SyncAlertServiceDataService # rubocop: disable CodeReuse/ServiceClass - .new(self) - .execute + # Stops execution of callbacks and database operation while + # preserving expectations of #save (will not raise) & #save! (raises) + # https://guides.rubyonrails.org/active_record_callbacks.html#halting-execution + throw :abort # rubocop:disable Cop/BanCatchThrow end end - -AlertsService.prepend_if_ee('EE::AlertsService') diff --git a/app/models/project_services/alerts_service_data.rb b/app/models/project_services/alerts_service_data.rb deleted file mode 100644 index 5a52ed83455..00000000000 --- a/app/models/project_services/alerts_service_data.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require 'securerandom' - -class AlertsServiceData < ApplicationRecord - belongs_to :service, class_name: 'AlertsService' - - validates :service, presence: true - - attr_encrypted :token, - mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_truncated, - algorithm: 'aes-256-gcm' -end diff --git a/app/models/project_services/datadog_service.rb b/app/models/project_services/datadog_service.rb index 543843ab1b0..3a742bfdcda 100644 --- a/app/models/project_services/datadog_service.rb +++ b/app/models/project_services/datadog_service.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true class DatadogService < Service - DEFAULT_SITE = 'datadoghq.com'.freeze - URL_TEMPLATE = 'https://webhooks-http-intake.logs.%{datadog_site}/v1/input/'.freeze - URL_TEMPLATE_API_KEYS = 'https://app.%{datadog_site}/account/settings#api'.freeze - URL_API_KEYS_DOCS = "https://docs.#{DEFAULT_SITE}/account_management/api-app-keys/".freeze + DEFAULT_SITE = 'datadoghq.com' + URL_TEMPLATE = 'https://webhooks-http-intake.logs.%{datadog_site}/v1/input/' + URL_TEMPLATE_API_KEYS = 'https://app.%{datadog_site}/account/settings#api' + URL_API_KEYS_DOCS = "https://docs.#{DEFAULT_SITE}/account_management/api-app-keys/" SUPPORTED_EVENTS = %w[ pipeline job diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 1f4abfc1aca..dafd3d095ec 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -157,8 +157,12 @@ class JiraService < IssueTrackerService # support any events. end + def find_issue(issue_key) + jira_request { client.Issue.find(issue_key) } + end + def close_issue(entity, external_issue) - issue = jira_request { client.Issue.find(external_issue.iid) } + issue = find_issue(external_issue.iid) return if issue.nil? || has_resolution?(issue) || !jira_issue_transition_id.present? @@ -172,7 +176,7 @@ class JiraService < IssueTrackerService # Depending on the Jira project's workflow, a comment during transition # may or may not be allowed. Refresh the issue after transition and check # if it is closed, so we don't have one comment for every commit. - issue = jira_request { client.Issue.find(issue.key) } if transition_issue(issue) + issue = find_issue(issue.key) if transition_issue(issue) add_issue_solved_comment(issue, commit_id, commit_url) if has_resolution?(issue) end @@ -181,7 +185,7 @@ class JiraService < IssueTrackerService return s_("JiraService|Events for %{noteable_model_name} are disabled.") % { noteable_model_name: noteable.model_name.plural.humanize(capitalize: false) } end - jira_issue = jira_request { client.Issue.find(mentioned.id) } + jira_issue = find_issue(mentioned.id) return unless jira_issue.present? diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 0fd85e3a5a9..f39d3947e5b 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class MattermostSlashCommandsService < SlashCommandsService - include TriggersHelper + include Ci::TriggersHelper prop_accessor :token diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb index 01ded0495a7..548f3623504 100644 --- a/app/models/project_services/slack_slash_commands_service.rb +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class SlackSlashCommandsService < SlashCommandsService - include TriggersHelper + include Ci::TriggersHelper def title 'Slack slash commands' diff --git a/app/models/protectable_dropdown.rb b/app/models/protectable_dropdown.rb index 25e70ab406c..e1336be9528 100644 --- a/app/models/protectable_dropdown.rb +++ b/app/models/protectable_dropdown.rb @@ -12,6 +12,8 @@ class ProtectableDropdown # Tags/branches which are yet to be individually protected def protectable_ref_names + return [] if @project.empty_repo? + @protectable_ref_names ||= ref_names - non_wildcard_protected_ref_names end diff --git a/app/models/release.rb b/app/models/release.rb index bebf91fb247..2b82fdc37f6 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -82,7 +82,7 @@ class Release < ApplicationRecord end def milestone_titles - self.milestones.map {|m| m.title }.sort.join(", ") + self.milestones.order_by_dates_and_title.map {|m| m.title }.join(', ') end def to_hook_data(action) diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index 6b8b34ce4d2..880970b72a8 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -308,7 +308,7 @@ class RemoteMirror < ApplicationRecord end def mirror_url_changed? - url_changed? || credentials_changed? + url_changed? || attribute_changed?(:credentials) end def saved_change_to_mirror_url? diff --git a/app/models/repository.rb b/app/models/repository.rb index 93f22dbe122..c19448332f8 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -24,10 +24,10 @@ class Repository attr_accessor :full_path, :shard, :disk_path, :container, :repo_type - delegate :ref_name_for_sha, to: :raw_repository - delegate :bundle_to_disk, to: :raw_repository delegate :lfs_enabled?, to: :container + delegate_missing_to :raw_repository + CreateTreeError = Class.new(StandardError) AmbiguousRefError = Class.new(StandardError) @@ -386,10 +386,6 @@ class Repository raw_repository.expire_has_local_branches_cache end - def lookup_cache - @lookup_cache ||= {} - end - def expire_exists_cache expire_method_caches(%i(exists?)) end @@ -494,19 +490,12 @@ class Repository expire_branches_cache if expire_cache end - def method_missing(msg, *args, &block) - if msg == :lookup && !block_given? - lookup_cache[msg] ||= {} - lookup_cache[msg][args.join(":")] ||= raw_repository.__send__(msg, *args, &block) # rubocop:disable GitlabSecurity/PublicSend - else - raw_repository.__send__(msg, *args, &block) # rubocop:disable GitlabSecurity/PublicSend + def lookup(sha) + strong_memoize("lookup_#{sha}") do + raw_repository.lookup(sha) end end - def respond_to_missing?(method, include_private = false) - raw_repository.respond_to?(method, include_private) || super - end - def blob_at(sha, path) blob = Blob.decorate(raw_repository.blob_at(sha, path), container) diff --git a/app/models/service.rb b/app/models/service.rb index 57c099d6f04..e5626462dd3 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -19,7 +19,6 @@ class Service < ApplicationRecord PROJECT_SPECIFIC_SERVICE_NAMES = %w[ jenkins - alerts ].freeze # Fake services to help with local development. @@ -48,7 +47,6 @@ class Service < ApplicationRecord after_commit :reset_updated_properties after_commit :cache_project_has_external_issue_tracker - after_commit :cache_project_has_external_wiki belongs_to :project, inverse_of: :services belongs_to :group, inverse_of: :services @@ -469,12 +467,6 @@ class Service < ApplicationRecord end end - def cache_project_has_external_wiki - if project && !project.destroyed? - project.cache_has_external_wiki - end - end - def valid_recipients? activated? && !importing? end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 817f9d014eb..c4a7c5e25dc 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -20,6 +20,7 @@ class Snippet < ApplicationRecord extend ::Gitlab::Utils::Override MAX_FILE_COUNT = 10 + MASTER_BRANCH = 'master' cache_markdown_field :title, pipeline: :single_line cache_markdown_field :description @@ -82,6 +83,7 @@ class Snippet < ApplicationRecord scope :inc_author, -> { includes(:author) } scope :inc_relations_for_view, -> { includes(author: :status) } scope :with_statistics, -> { joins(:statistics) } + scope :inc_projects_namespace_route, -> { includes(project: [:route, :namespace]) } attr_mentionable :description @@ -311,13 +313,27 @@ class Snippet < ApplicationRecord override :default_branch def default_branch - super || 'master' + super || MASTER_BRANCH end def repository_storage snippet_repository&.shard_name || self.class.pick_repository_storage end + # Repositories are created by default with the `master` branch. + # This method changes the `HEAD` file to point to the existing + # default branch in case it's not master. + def change_head_to_default_branch + return unless repository.exists? + return if default_branch == MASTER_BRANCH + # All snippets must have at least 1 file. Therefore, if + # `HEAD` is empty is because it's pointing to the wrong + # default branch + return unless repository.empty? || list_files('HEAD').empty? + + repository.raw_repository.write_ref('HEAD', "refs/heads/#{default_branch}") + end + def create_repository return if repository_exists? && snippet_repository diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb index fa25a6f8441..54dbc579d54 100644 --- a/app/models/snippet_repository.rb +++ b/app/models/snippet_repository.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class SnippetRepository < ApplicationRecord + include EachBatch include Shardable DEFAULT_EMPTY_FILE_NAME = 'snippetfile' diff --git a/app/models/snippet_repository_storage_move.rb b/app/models/snippet_repository_storage_move.rb index a365569bfa8..bb157c08995 100644 --- a/app/models/snippet_repository_storage_move.rb +++ b/app/models/snippet_repository_storage_move.rb @@ -12,7 +12,11 @@ class SnippetRepositoryStorageMove < ApplicationRecord override :schedule_repository_storage_update_worker def schedule_repository_storage_update_worker - # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/218991 + SnippetUpdateRepositoryStorageWorker.perform_async( + snippet_id, + destination_storage_name, + id + ) end private diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb index 1b99f310e1a..efbbd86ae4a 100644 --- a/app/models/terraform/state.rb +++ b/app/models/terraform/state.rb @@ -27,6 +27,8 @@ module Terraform validates :uuid, presence: true, uniqueness: true, length: { is: UUID_LENGTH }, format: { with: HEX_REGEXP, message: 'only allows hex characters' } + before_destroy :ensure_state_is_unlocked + default_value_for(:uuid, allows_nil: false) { SecureRandom.hex(UUID_LENGTH / 2) } def latest_file @@ -87,6 +89,13 @@ module Terraform new_version.save! end + def ensure_state_is_unlocked + return unless locked? + + errors.add(:base, s_("Terraform|You cannot remove the State file because it's locked. Unlock the State file first before removing it.")) + throw :abort # rubocop:disable Cop/BanCatchThrow + end + def parse_serial(file) Gitlab::Json.parse(file)["serial"] rescue JSON::ParserError diff --git a/app/models/user.rb b/app/models/user.rb index c735f20b92c..b4ec6064ff8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -31,7 +31,7 @@ class User < ApplicationRecord INSTANCE_ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10 - BLOCKED_PENDING_APPROVAL_STATE = 'blocked_pending_approval'.freeze + BLOCKED_PENDING_APPROVAL_STATE = 'blocked_pending_approval' add_authentication_token_field :incoming_email_token, token_generator: -> { SecureRandom.hex.to_i(16).to_s(36) } add_authentication_token_field :feed_token @@ -1358,6 +1358,7 @@ class User < ApplicationRecord def hook_attrs { + id: id, name: name, username: username, avatar_url: avatar_url(only_path: false), @@ -1377,7 +1378,14 @@ class User < ApplicationRecord def set_username_errors namespace_path_errors = self.errors.delete(:"namespace.path") - self.errors[:username].concat(namespace_path_errors) if namespace_path_errors + + return unless namespace_path_errors&.any? + + if namespace_path_errors.include?('has already been taken') && !User.exists?(username: username) + self.errors.add(:base, :username_exists_as_a_different_namespace) + else + self.errors[:username].concat(namespace_path_errors) + end end def username_changed_hook @@ -1564,6 +1572,12 @@ class User < ApplicationRecord end end + def review_requested_open_merge_requests_count(force: false) + Rails.cache.fetch(['users', id, 'review_requested_open_merge_requests_count'], force: force, expires_in: 20.minutes) do + MergeRequestsFinder.new(self, reviewer_id: id, state: 'opened', non_archived: true).execute.count + end + end + def assigned_open_issues_count(force: false) Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do IssuesFinder.new(self, assignee_id: self.id, state: 'opened', non_archived: true).execute.count @@ -1607,6 +1621,7 @@ class User < ApplicationRecord def invalidate_merge_request_cache_counts Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count']) + Rails.cache.delete(['users', id, 'review_requested_open_merge_requests_count']) end def invalidate_todos_done_count diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index b49a7eb72dc..49b93ffaf66 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -8,8 +8,6 @@ class UserPreference < ApplicationRecord # extra methods that aren't really needed here. NOTES_FILTERS = { all_notes: 0, only_comments: 1, only_activity: 2 }.freeze - ignore_column :feature_filter_type, remove_with: '13.8', remove_after: '2021-01-22' - belongs_to :user scope :with_user, -> { joins(:user) } diff --git a/app/models/wiki.rb b/app/models/wiki.rb index e329a094319..11c10a61d18 100644 --- a/app/models/wiki.rb +++ b/app/models/wiki.rb @@ -3,6 +3,7 @@ class Wiki extend ::Gitlab::Utils::Override include HasRepository + include Repositories::CanHousekeepRepository include Gitlab::Utils::StrongMemoize include GlobalID::Identification diff --git a/app/policies/analytics/instance_statistics/measurement_policy.rb b/app/policies/analytics/instance_statistics/measurement_policy.rb new file mode 100644 index 00000000000..3d6a5a08ff6 --- /dev/null +++ b/app/policies/analytics/instance_statistics/measurement_policy.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Analytics + module InstanceStatistics + class MeasurementPolicy < BasePolicy + delegate { :global } + end + end +end diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 7e69e1fdd88..65f2a70672b 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -37,6 +37,10 @@ module Ci @subject.archived? end + condition(:artifacts_public, scope: :subject) do + @subject.artifacts_public? + end + condition(:terminal, scope: :subject) do @subject.has_terminal? end @@ -57,6 +61,10 @@ module Ci can?(:update_build, @subject.project) end + condition(:project_developer) do + can?(:developer_access, @subject.project) + end + rule { project_read_build }.enable :read_build_trace rule { debug_mode & ~project_update_build }.prevent :read_build_trace @@ -94,6 +102,9 @@ module Ci rule { ~can?(:build_service_proxy_enabled) }.policy do prevent :create_build_service_proxy end + + rule { project_read_build }.enable :read_job_artifacts + rule { ~artifacts_public & ~project_developer }.prevent :read_job_artifacts end end diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index b5c1ec0181e..9c79a797a6a 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -100,6 +100,7 @@ class GlobalPolicy < BasePolicy enable :update_custom_attribute enable :approve_user enable :reject_user + enable :read_instance_statistics_measurements end # We can't use `read_statistics` because the user may have different permissions for different projects diff --git a/app/policies/group_member_policy.rb b/app/policies/group_member_policy.rb index 78a2be7a9f8..09cac96e3a5 100644 --- a/app/policies/group_member_policy.rb +++ b/app/policies/group_member_policy.rb @@ -10,7 +10,11 @@ class GroupMemberPolicy < BasePolicy with_score 0 condition(:is_target_user) { @user && @subject.user_id == @user.id } - rule { anonymous }.prevent_all + rule { anonymous }.policy do + prevent :update_group_member + prevent :destroy_group_member + end + rule { last_owner }.policy do prevent :update_group_member prevent :destroy_group_member diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 7d0db222eaf..7dd88fcc1ff 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -66,7 +66,7 @@ class GroupPolicy < BasePolicy with_scope :subject condition(:has_project_with_service_desk_enabled) { @subject.has_project_with_service_desk_enabled? } - rule { design_management_enabled }.policy do + rule { can?(:read_group) & design_management_enabled }.policy do enable :read_design_activity end @@ -116,17 +116,20 @@ class GroupPolicy < BasePolicy enable :delete_metrics_dashboard_annotation enable :update_metrics_dashboard_annotation enable :create_custom_emoji + enable :create_package_settings end rule { reporter }.policy do enable :reporter_access enable :read_container_image + enable :admin_board enable :admin_label enable :admin_list enable :admin_issue enable :read_metrics_dashboard_annotation enable :read_prometheus enable :read_package + enable :read_package_settings end rule { maintainer }.policy do diff --git a/app/policies/namespace/package_setting_policy.rb b/app/policies/namespace/package_setting_policy.rb new file mode 100644 index 00000000000..7fe388c633e --- /dev/null +++ b/app/policies/namespace/package_setting_policy.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Namespace::PackageSettingPolicy < BasePolicy + delegate { @subject.namespace } +end diff --git a/app/policies/namespace_policy.rb b/app/policies/namespace_policy.rb index b1d680b4264..13eb4a13cac 100644 --- a/app/policies/namespace_policy.rb +++ b/app/policies/namespace_policy.rb @@ -14,6 +14,8 @@ class NamespacePolicy < BasePolicy enable :read_namespace enable :read_statistics enable :create_jira_connect_subscription + enable :create_package_settings + enable :read_package_settings end rule { personal_project & ~can_create_personal_project }.prevent :create_projects diff --git a/app/policies/packages/composer/metadatum_policy.rb b/app/policies/packages/composer/metadatum_policy.rb new file mode 100644 index 00000000000..66bac31f48f --- /dev/null +++ b/app/policies/packages/composer/metadatum_policy.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +module Packages + module Composer + class MetadatumPolicy < BasePolicy + delegate { @subject.package } + end + end +end diff --git a/app/policies/packages/tag_policy.rb b/app/policies/packages/tag_policy.rb new file mode 100644 index 00000000000..84bad30470a --- /dev/null +++ b/app/policies/packages/tag_policy.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +module Packages + class TagPolicy < BasePolicy + delegate { @subject.package } + end +end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 3b8c59c6bf8..03cb53f55be 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -135,6 +135,10 @@ class ProjectPolicy < BasePolicy ::Feature.enabled?(:build_service_proxy, @subject) end + condition(:user_defined_variables_allowed) do + !@subject.restrict_user_defined_variables? + end + with_scope :subject condition(:packages_disabled) { !@subject.packages_enabled } @@ -236,6 +240,7 @@ class ProjectPolicy < BasePolicy enable :read_commit_status enable :read_build enable :read_container_image + enable :read_deploy_board enable :read_pipeline enable :read_pipeline_schedule enable :read_environment @@ -615,6 +620,10 @@ class ProjectPolicy < BasePolicy enable :admin_resource_access_tokens end + rule { user_defined_variables_allowed | can?(:maintainer_access) }.policy do + enable :set_pipeline_variables + end + private def user_is_user? diff --git a/app/presenters/board_presenter.rb b/app/presenters/board_presenter.rb new file mode 100644 index 00000000000..d7cecd44dd7 --- /dev/null +++ b/app/presenters/board_presenter.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class BoardPresenter < Gitlab::View::Presenter::Delegated + presents :board +end diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb index 64461fa9193..ffa33dc9f15 100644 --- a/app/presenters/ci/build_runner_presenter.rb +++ b/app/presenters/ci/build_runner_presenter.rb @@ -93,7 +93,22 @@ module Ci end def refspec_for_persistent_ref - "+#{persistent_ref_path}:#{persistent_ref_path}" + # + # End-to-end test coverage for CI fetching seems to not be strong, so we + # are using a feature flag here to close the confidence gap. My (JV) + # confidence about the change is very high but if something is wrong + # with it after all, this would cause all CI jobs on gitlab.com to fail. + # + # The roll-out will be tracked in + # https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/746. + # + if Feature.enabled?(:scalability_ci_fetch_sha, type: :ops) + # Use persistent_ref.sha because it causes 'git fetch' to do less work. + # See https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/746. + "+#{pipeline.persistent_ref.sha}:#{pipeline.persistent_ref.path}" + else + "+#{pipeline.persistent_ref.path}:#{pipeline.persistent_ref.path}" + end end def persistent_ref_exist? @@ -107,10 +122,6 @@ module Ci pipeline.persistent_ref.exist? end - def persistent_ref_path - pipeline.persistent_ref.path - end - def git_depth_variable strong_memoize(:git_depth_variable) do variables&.find { |variable| variable[:key] == 'GIT_DEPTH' } diff --git a/app/presenters/packages/composer/packages_presenter.rb b/app/presenters/packages/composer/packages_presenter.rb index cce006cbb1a..ed0e9d3b731 100644 --- a/app/presenters/packages/composer/packages_presenter.rb +++ b/app/presenters/packages/composer/packages_presenter.rb @@ -20,33 +20,17 @@ module Packages end def package_versions(packages = @packages) - { 'packages' => { packages.first.name => package_versions_map(packages) } } + package_versions_index(packages).as_json end private - def package_versions_map(packages) - packages.each_with_object({}) do |package, map| - map[package.version] = package_metadata(package) - end + def package_versions_sha(packages = @packages) + package_versions_index(packages).sha end - def package_metadata(package) - json = package.composer_metadatum.composer_json - - json.merge('dist' => package_dist(package), 'uid' => package.id, 'version' => package.version) - end - - def package_dist(package) - sha = package.composer_metadatum.target_sha - archive_api_path = api_v4_projects_packages_composer_archives_package_name_path({ id: package.project_id, package_name: package.name, format: '.zip' }, true) - - { - 'type' => 'zip', - 'url' => expose_url(archive_api_path) + "?sha=#{sha}", - 'reference' => sha, - 'shasum' => '' - } + def package_versions_index(packages) + ::Gitlab::Composer::VersionIndex.new(packages) end def providers_map @@ -59,10 +43,6 @@ module Packages map end - def package_versions_sha(packages) - Digest::SHA256.hexdigest(package_versions(packages).to_json) - end - def provider_sha Digest::SHA256.hexdigest(provider.to_json) end diff --git a/app/presenters/packages/nuget/service_index_presenter.rb b/app/presenters/packages/nuget/service_index_presenter.rb index ed00b36b362..b3cc912b811 100644 --- a/app/presenters/packages/nuget/service_index_presenter.rb +++ b/app/presenters/packages/nuget/service_index_presenter.rb @@ -19,10 +19,13 @@ module Packages metadata: 'Get package metadata.' }.freeze - VERSION = '3.0.0'.freeze + VERSION = '3.0.0' - def initialize(project) - @project = project + PROJECT_LEVEL_SERVICES = %i[download publish].freeze + GROUP_LEVEL_SERVICES = %i[search metadata].freeze + + def initialize(project_or_group) + @project_or_group = project_or_group end def version @@ -30,16 +33,21 @@ module Packages end def resources - [ - build_service(:download), - build_service(:search), - build_service(:publish), - build_service(:metadata) - ].flatten + available_services.map { |service| build_service(service) } + .flatten end private + def available_services + case scope + when :group + GROUP_LEVEL_SERVICES + when :project + (GROUP_LEVEL_SERVICES + PROJECT_LEVEL_SERVICES).flatten + end + end + def build_service(service_type) url = build_service_url(service_type) comment = SERVICE_COMMENTS[service_type] @@ -50,36 +58,72 @@ module Packages end def build_service_url(service_type) - base_path = api_v4_projects_packages_nuget_path(id: @project.id) - full_path = case service_type when :download - api_v4_projects_packages_nuget_download_package_name_package_version_package_filename_path( - { - id: @project.id, - package_name: nil, - package_version: nil, - package_filename: nil - }, - true - ) + download_service_url when :search - "#{base_path}/query" + search_service_url when :metadata - api_v4_projects_packages_nuget_metadata_package_name_package_version_path( - { - id: @project.id, - package_name: nil, - package_version: nil - }, - true - ) + metadata_service_url when :publish - base_path + publish_service_url end expose_url(full_path) end + + def scope + return :project if @project_or_group.is_a?(::Project) + return :group if @project_or_group.is_a?(::Group) + end + + def download_service_url + params = { + id: @project_or_group.id, + package_name: nil, + package_version: nil, + package_filename: nil + } + + api_v4_projects_packages_nuget_download_package_name_package_version_package_filename_path( + params, + true + ) + end + + def metadata_service_url + params = { + id: @project_or_group.id, + package_name: nil, + package_version: nil + } + + case scope + when :group + api_v4_groups___packages_nuget_metadata_package_name_package_version_path( + params, + true + ) + when :project + api_v4_projects_packages_nuget_metadata_package_name_package_version_path( + params, + true + ) + end + end + + def search_service_url + case scope + when :group + api_v4_groups___packages_nuget_query_path(id: @project_or_group.id) + when :project + api_v4_projects_packages_nuget_query_path(id: @project_or_group.id) + end + end + + def publish_service_url + api_v4_projects_packages_nuget_path(id: @project_or_group.id) + end end end end diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 917c416ce33..2432a6a0e4d 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -26,7 +26,7 @@ class BuildDetailsEntity < JobEntity DeploymentClusterEntity.represent(build.deployment, options) end - expose :artifact, if: -> (*) { can?(current_user, :read_build, build) } do + expose :artifact, if: -> (*) { can?(current_user, :read_job_artifacts, build) } do expose :download_path, if: -> (*) { build.locked_artifacts? || build.artifacts? } do |build| download_project_job_artifacts_path(project, build) end diff --git a/app/serializers/diffs_metadata_entity.rb b/app/serializers/diffs_metadata_entity.rb index 7b0de3bce4e..681e629244f 100644 --- a/app/serializers/diffs_metadata_entity.rb +++ b/app/serializers/diffs_metadata_entity.rb @@ -18,8 +18,30 @@ class DiffsMetadataEntity < DiffsEntity options[:merge_request].can_be_merged_by?(request.current_user) end + expose :project_path + expose :project_name + + expose :username + expose :user_full_name + private + def project_path + request.project&.full_path + end + + def project_name + request.project&.name + end + + def username + request.current_user&.username + end + + def user_full_name + request.current_user&.name + end + def presenter(merge_request) @presenters ||= {} @presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: request.current_user) # rubocop: disable CodeReuse/Presenter diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index 8c6ad010d69..66ca2382901 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -19,6 +19,7 @@ class EnvironmentEntity < Grape::Entity expose :name_without_type expose :last_deployment, using: DeploymentEntity expose :stop_action_available?, as: :has_stop_action + expose :rollout_status, if: -> (*) { can_read_deploy_board? }, using: RolloutStatusEntity expose :upcoming_deployment, expose_nil: false do |environment, ops| DeploymentEntity.represent(environment.upcoming_deployment, @@ -104,6 +105,10 @@ class EnvironmentEntity < Grape::Entity can?(current_user, :read_pod_logs, environment.project) end + def can_read_deploy_board? + can?(current_user, :read_deploy_board, environment.project) + end + def cluster_platform_kubernetes? deployment_platform && deployment_platform.is_a?(Clusters::Platforms::Kubernetes) end diff --git a/app/serializers/group_analytics_stage_entity.rb b/app/serializers/group_analytics_stage_entity.rb deleted file mode 100644 index 81be20e7dd8..00000000000 --- a/app/serializers/group_analytics_stage_entity.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -class GroupAnalyticsStageEntity < Grape::Entity - include EntityDateHelper - - expose :title - expose :name - expose :legend - expose :description - - expose :group_median, as: :value do |stage| - # group_median returns a BatchLoader instance which we first have to unwrap by using to_f - # we use to_f to make sure results below 1 are presented to the end-user - stage.group_median.to_f.nonzero? ? distance_of_time_in_words(stage.group_median) : nil - end -end diff --git a/app/serializers/group_analytics_stage_serializer.rb b/app/serializers/group_analytics_stage_serializer.rb deleted file mode 100644 index ec448dea602..00000000000 --- a/app/serializers/group_analytics_stage_serializer.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -class GroupAnalyticsStageSerializer < BaseSerializer - entity GroupAnalyticsStageEntity -end diff --git a/app/serializers/issuable_sidebar_basic_entity.rb b/app/serializers/issuable_sidebar_basic_entity.rb index 53c123c06fd..4b3d6f21d6d 100644 --- a/app/serializers/issuable_sidebar_basic_entity.rb +++ b/app/serializers/issuable_sidebar_basic_entity.rb @@ -103,6 +103,10 @@ class IssuableSidebarBasicEntity < Grape::Entity issuable.project.emails_disabled? end + expose :create_note_email do |issuable| + issuable.creatable_note_email_address(current_user) + end + expose :supports_time_tracking?, as: :supports_time_tracking expose :supports_milestone?, as: :supports_milestone expose :supports_severity?, as: :supports_severity diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb index 7c12e0956f3..647a73495f8 100644 --- a/app/serializers/issue_entity.rb +++ b/app/serializers/issue_entity.rb @@ -71,6 +71,10 @@ class IssueEntity < IssuableEntity expose :archived_project_docs_path, if: -> (issue) { issue.project.archived? } do |issue| help_page_path('user/project/settings/index.md', anchor: 'archiving-a-project') end + + expose :issue_email_participants do |issue| + issue.issue_email_participants.map { |x| { email: x.email } } + end end IssueEntity.prepend_if_ee('::EE::IssueEntity') diff --git a/app/serializers/member_entity.rb b/app/serializers/member_entity.rb new file mode 100644 index 00000000000..584ba4c62de --- /dev/null +++ b/app/serializers/member_entity.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +class MemberEntity < Grape::Entity + include RequestAwareEntity + include AvatarsHelper + + expose :id + expose :created_at + expose :expires_at do |member| + member.expires_at&.to_time + end + expose :requested_at + + expose :created_by, if: -> (member) { member.created_by.present? } do |member| + UserEntity.represent(member.created_by, only: [:name, :web_url]) + end + + expose :can_update do |member| + member.can_update? + end + + expose :can_remove do |member| + member.can_remove? + end + + expose :access_level do + expose :human_access, as: :string_value + expose :access_level, as: :integer_value + end + + expose :source do |member| + GroupEntity.represent(member.source, only: [:id, :full_name, :web_url]) + end + + expose :valid_level_roles, as: :valid_roles + + expose :user, if: -> (member) { member.user.present? }, using: MemberUserEntity + + expose :invite, if: -> (member) { member.invite? } do + expose :email do |member| + member.invite_email + end + + expose :avatar_url do |member| + avatar_icon_for_email(member.invite_email, Member::AVATAR_SIZE) + end + + expose :can_resend do |member| + member.can_resend_invite? + end + end +end + +MemberEntity.prepend_if_ee('EE::MemberEntity') diff --git a/app/serializers/member_serializer.rb b/app/serializers/member_serializer.rb new file mode 100644 index 00000000000..b34d7f30a58 --- /dev/null +++ b/app/serializers/member_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class MemberSerializer < BaseSerializer + entity MemberEntity +end diff --git a/app/serializers/member_user_entity.rb b/app/serializers/member_user_entity.rb new file mode 100644 index 00000000000..a022966c041 --- /dev/null +++ b/app/serializers/member_user_entity.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class MemberUserEntity < UserEntity + unexpose :show_status + unexpose :path + unexpose :state + unexpose :status_tooltip_html + + expose :avatar_url do |user| + user.avatar_url(size: Member::AVATAR_SIZE, only_path: false) + end + + expose :blocked do |user| + user.blocked? + end + + expose :two_factor_enabled do |user| + user.two_factor_enabled? + end + + expose :status, if: -> (user) { user.status.present? } do + expose :emoji do |user| + user.status.emoji + end + end +end + +MemberUserEntity.prepend_if_ee('EE::MemberUserEntity') diff --git a/app/serializers/merge_request_poll_cached_widget_entity.rb b/app/serializers/merge_request_poll_cached_widget_entity.rb index 080b6554de1..1db4ec37d4a 100644 --- a/app/serializers/merge_request_poll_cached_widget_entity.rb +++ b/app/serializers/merge_request_poll_cached_widget_entity.rb @@ -104,6 +104,16 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity presenter(merge_request).api_unapprove_path end + expose :blob_path do + expose :head_path, if: -> (mr, _) { mr.source_branch_sha } do |merge_request| + project_blob_path(merge_request.project, merge_request.source_branch_sha) + end + + expose :base_path, if: -> (mr, _) { mr.diff_base_sha } do |merge_request| + project_blob_path(merge_request.project, merge_request.diff_base_sha) + end + end + private delegate :current_user, to: :request diff --git a/app/serializers/merge_request_poll_widget_entity.rb b/app/serializers/merge_request_poll_widget_entity.rb index 9609a894e6d..4c34da3fc88 100644 --- a/app/serializers/merge_request_poll_widget_entity.rb +++ b/app/serializers/merge_request_poll_widget_entity.rb @@ -85,6 +85,12 @@ class MergeRequestPollWidgetEntity < Grape::Entity end end + expose :codequality_reports_path do |merge_request| + if merge_request.has_codequality_reports? + codequality_reports_project_merge_request_path(merge_request.project, merge_request, format: :json) + end + end + expose :terraform_reports_path do |merge_request| if merge_request.has_terraform_reports? terraform_reports_project_merge_request_path(merge_request.project, merge_request, format: :json) diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index afd4d5b9a2b..ca4e16bc5ff 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -115,16 +115,6 @@ class MergeRequestWidgetEntity < Grape::Entity end end - expose :blob_path do - expose :head_path, if: -> (mr, _) { mr.source_branch_sha } do |merge_request| - project_blob_path(merge_request.project, merge_request.source_branch_sha) - end - - expose :base_path, if: -> (mr, _) { mr.diff_base_sha } do |merge_request| - project_blob_path(merge_request.project, merge_request.diff_base_sha) - end - end - expose :codeclimate, if: -> (mr, _) { head_pipeline_downloadable_path_for_report_type(:codequality) } do expose :head_path do |merge_request| head_pipeline_downloadable_path_for_report_type(:codequality) diff --git a/app/serializers/merge_requests/pipeline_entity.rb b/app/serializers/merge_requests/pipeline_entity.rb index 97d7620154e..8b684d4641b 100644 --- a/app/serializers/merge_requests/pipeline_entity.rb +++ b/app/serializers/merge_requests/pipeline_entity.rb @@ -21,6 +21,16 @@ class MergeRequests::PipelineEntity < Grape::Entity pipeline.present.name end + expose :artifacts do |pipeline, options| + rel = pipeline.downloadable_artifacts + + if Feature.enabled?(:non_public_artifacts, type: :development) + rel = rel.select { |artifact| can?(request.current_user, :read_job_artifacts, artifact.job) } + end + + BuildArtifactEntity.represent(rel, options) + end + expose :detailed_status, as: :status, with: DetailedStatusEntity do |pipeline| pipeline.detailed_status(request.current_user) end diff --git a/app/serializers/pipeline_details_entity.rb b/app/serializers/pipeline_details_entity.rb index 50efa9ea15d..e53fa7873ac 100644 --- a/app/serializers/pipeline_details_entity.rb +++ b/app/serializers/pipeline_details_entity.rb @@ -11,6 +11,10 @@ class PipelineDetailsEntity < PipelineEntity expose :artifacts do |pipeline, options| rel = pipeline.downloadable_artifacts + if Feature.enabled?(:non_public_artifacts, type: :development) + rel = rel.select { |artifact| can?(request.current_user, :read_job_artifacts, artifact.job) } + end + BuildArtifactEntity.represent(rel, options) end expose :manual_actions, using: BuildActionEntity diff --git a/app/services/alert_management/http_integrations/create_service.rb b/app/services/alert_management/http_integrations/create_service.rb index 576e38c23aa..e7f1084ce5c 100644 --- a/app/services/alert_management/http_integrations/create_service.rb +++ b/app/services/alert_management/http_integrations/create_service.rb @@ -9,14 +9,14 @@ module AlertManagement def initialize(project, current_user, params) @project = project @current_user = current_user - @params = params + @params = params.with_indifferent_access end def execute return error_no_permissions unless allowed? return error_multiple_integrations unless creation_allowed? - integration = project.alert_management_http_integrations.create(params) + integration = project.alert_management_http_integrations.create(permitted_params) return error_in_create(integration) unless integration.valid? success(integration) @@ -34,6 +34,15 @@ module AlertManagement project.alert_management_http_integrations.empty? end + def permitted_params + params.slice(*permitted_params_keys) + end + + # overriden in EE + def permitted_params_keys + %i[name active] + end + def error(message) ServiceResponse.error(message: message) end diff --git a/app/services/alert_management/sync_alert_service_data_service.rb b/app/services/alert_management/sync_alert_service_data_service.rb deleted file mode 100644 index 1ba197065c5..00000000000 --- a/app/services/alert_management/sync_alert_service_data_service.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -module AlertManagement - class SyncAlertServiceDataService - # @param alert_service [AlertsService] - def initialize(alert_service) - @alert_service = alert_service - end - - def execute - http_integration = find_http_integration - - result = if http_integration - update_integration_data(http_integration) - else - create_integration - end - - result ? ServiceResponse.success : ServiceResponse.error(message: 'Update failed') - end - - private - - attr_reader :alert_service - - def find_http_integration - AlertManagement::HttpIntegrationsFinder.new( - alert_service.project, - endpoint_identifier: ::AlertManagement::HttpIntegration::LEGACY_IDENTIFIER - ) - .execute - .first - end - - def create_integration - new_integration = AlertManagement::HttpIntegration.create( - project_id: alert_service.project_id, - name: 'HTTP endpoint', - endpoint_identifier: AlertManagement::HttpIntegration::LEGACY_IDENTIFIER, - active: alert_service.active, - encrypted_token: alert_service.data.encrypted_token, - encrypted_token_iv: alert_service.data.encrypted_token_iv - ) - - new_integration.persisted? - end - - def update_integration_data(http_integration) - http_integration.update( - active: alert_service.active, - encrypted_token: alert_service.data.encrypted_token, - encrypted_token_iv: alert_service.data.encrypted_token_iv - ) - end - end -end diff --git a/app/services/boards/base_items_list_service.rb b/app/services/boards/base_items_list_service.rb new file mode 100644 index 00000000000..851120ef597 --- /dev/null +++ b/app/services/boards/base_items_list_service.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +module Boards + class BaseItemsListService < Boards::BaseService + include Gitlab::Utils::StrongMemoize + include ActiveRecord::ConnectionAdapters::Quoting + + def execute + return items.order_closed_date_desc if list&.closed? + + ordered_items + end + + private + + def ordered_items + raise NotImplementedError + end + + def finder + raise NotImplementedError + end + + def board + raise NotImplementedError + end + + def item_model + raise NotImplementedError + end + + # We memoize the query here since the finder methods we use are quite complex. This does not memoize the result of the query. + # rubocop: disable CodeReuse/ActiveRecord + def items + strong_memoize(:items) do + filter(finder.execute).reorder(nil) + end + end + # rubocop: enable CodeReuse/ActiveRecord + + def filter(items) + # when grouping board issues by epics (used in board swimlanes) + # we need to get all issues in the board + # TODO: ignore hidden columns - + # https://gitlab.com/gitlab-org/gitlab/-/issues/233870 + return items if params[:all_lists] + + items = without_board_labels(items) unless list&.movable? || list&.closed? + items = with_list_label(items) if list&.label? + items + end + + def list + return unless params.key?(:id) + + strong_memoize(:list) do + id = params[:id] + + if board.lists.loaded? + board.lists.find { |l| l.id == id } + else + board.lists.find(id) + end + end + end + + def filter_params + set_parent + set_state + set_attempt_search_optimizations + + params + end + + def set_parent + if parent.is_a?(Group) + params[:group_id] = parent.id + else + params[:project_id] = parent.id + end + end + + def set_state + return if params[:all_lists] + + params[:state] = list && list.closed? ? 'closed' : 'opened' + end + + def set_attempt_search_optimizations + return unless params[:search].present? + + if board.group_board? + params[:attempt_group_search_optimizations] = true + else + params[:attempt_project_search_optimizations] = true + end + end + + # rubocop: disable CodeReuse/ActiveRecord + def board_label_ids + @board_label_ids ||= board.lists.movable.pluck(:label_id) + end + # rubocop: enable CodeReuse/ActiveRecord + + # rubocop: disable CodeReuse/ActiveRecord + def without_board_labels(items) + return items unless board_label_ids.any? + + items.where.not('EXISTS (?)', label_links(board_label_ids).limit(1)) + end + # rubocop: enable CodeReuse/ActiveRecord + + # rubocop: disable CodeReuse/ActiveRecord + def label_links(label_ids) + LabelLink + .where('label_links.target_type = ?', item_model) + .where(item_model.arel_table[:id].eq(LabelLink.arel_table[:target_id]).to_sql) + .where(label_id: label_ids) + end + # rubocop: enable CodeReuse/ActiveRecord + + # rubocop: disable CodeReuse/ActiveRecord + def with_list_label(items) + items.where('EXISTS (?)', label_links(list.label_id).limit(1)) + end + # rubocop: enable CodeReuse/ActiveRecord + end +end diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index ab9d11abe98..27d59e052c7 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -2,26 +2,20 @@ module Boards module Issues - class ListService < Boards::BaseService + class ListService < Boards::BaseItemsListService include Gitlab::Utils::StrongMemoize def self.valid_params IssuesFinder.valid_params end - def execute - return fetch_issues.order_closed_date_desc if list&.closed? - - fetch_issues.order_by_position_and_priority(with_cte: params[:search].present?) - end - # rubocop: disable CodeReuse/ActiveRecord def metadata issues = Issue.arel_table keys = metadata_fields.keys # TODO: eliminate need for SQL literal fragment columns = Arel.sql(metadata_fields.values_at(*keys).join(', ')) - results = Issue.where(id: fetch_issues.select(issues[:id])).pluck(columns) + results = Issue.where(id: items.select(issues[:id])).pluck(columns) Hash[keys.zip(results.flatten)] end @@ -29,74 +23,28 @@ module Boards private - def metadata_fields - { size: 'COUNT(*)' } - end - - # We memoize the query here since the finder methods we use are quite complex. This does not memoize the result of the query. - # rubocop: disable CodeReuse/ActiveRecord - def fetch_issues - strong_memoize(:fetch_issues) do - issues = IssuesFinder.new(current_user, filter_params).execute - - filter(issues).reorder(nil) - end + def ordered_items + items.order_by_position_and_priority(with_cte: params[:search].present?) end - # rubocop: enable CodeReuse/ActiveRecord - def filter(issues) - # when grouping board issues by epics (used in board swimlanes) - # we need to get all issues in the board - # TODO: ignore hidden columns - - # https://gitlab.com/gitlab-org/gitlab/-/issues/233870 - return issues if params[:all_lists] - - issues = without_board_labels(issues) unless list&.movable? || list&.closed? - issues = with_list_label(issues) if list&.label? - issues + def finder + IssuesFinder.new(current_user, filter_params) end def board @board ||= parent.boards.find(params[:board_id]) end - def list - return unless params.key?(:id) - - strong_memoize(:list) do - id = params[:id] - - if board.lists.loaded? - board.lists.find { |l| l.id == id } - else - board.lists.find(id) - end - end + def metadata_fields + { size: 'COUNT(*)' } end def filter_params - set_parent - set_state set_scope set_non_archived - set_attempt_search_optimizations set_issue_types - params - end - - def set_parent - if parent.is_a?(Group) - params[:group_id] = parent.id - else - params[:project_id] = parent.id - end - end - - def set_state - return if params[:all_lists] - - params[:state] = list && list.closed? ? 'closed' : 'opened' + super end def set_scope @@ -107,49 +55,12 @@ module Boards params[:non_archived] = parent.is_a?(Group) end - def set_attempt_search_optimizations - return unless params[:search].present? - - if board.group_board? - params[:attempt_group_search_optimizations] = true - else - params[:attempt_project_search_optimizations] = true - end - end - def set_issue_types params[:issue_types] = Issue::TYPES_FOR_LIST end - # rubocop: disable CodeReuse/ActiveRecord - def board_label_ids - @board_label_ids ||= board.lists.movable.pluck(:label_id) - end - # rubocop: enable CodeReuse/ActiveRecord - - # rubocop: disable CodeReuse/ActiveRecord - def without_board_labels(issues) - return issues unless board_label_ids.any? - - issues.where.not('EXISTS (?)', issues_label_links.limit(1)) - end - # rubocop: enable CodeReuse/ActiveRecord - - # rubocop: disable CodeReuse/ActiveRecord - def issues_label_links - LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id").where(label_id: board_label_ids) - end - # rubocop: enable CodeReuse/ActiveRecord - - # rubocop: disable CodeReuse/ActiveRecord - def with_list_label(issues) - issues.where('EXISTS (?)', LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id") - .where("label_links.label_id = ?", list.label_id).limit(1)) - end - # rubocop: enable CodeReuse/ActiveRecord - - def board_group - board.group_board? ? parent : parent.group + def item_model + Issue end end end diff --git a/app/services/bulk_create_integration_service.rb b/app/services/bulk_create_integration_service.rb index 61c5565db60..df78c3645c7 100644 --- a/app/services/bulk_create_integration_service.rb +++ b/app/services/bulk_create_integration_service.rb @@ -38,10 +38,6 @@ class BulkCreateIntegrationService if integration.external_issue_tracker? Project.where(id: batch.select(:id)).update_all(has_external_issue_tracker: true) end - - if integration.external_wiki? - Project.where(id: batch.select(:id)).update_all(has_external_wiki: true) - end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/services/ci/create_downstream_pipeline_service.rb b/app/services/ci/create_downstream_pipeline_service.rb index 86d0cf079fc..629d85b041f 100644 --- a/app/services/ci/create_downstream_pipeline_service.rb +++ b/app/services/ci/create_downstream_pipeline_service.rb @@ -33,9 +33,7 @@ module Ci pipeline_params.fetch(:target_revision)) downstream_pipeline = service.execute( - pipeline_params.fetch(:source), **pipeline_params[:execute_params]) do |pipeline| - pipeline.variables.build(@bridge.downstream_variables) - end + pipeline_params.fetch(:source), **pipeline_params[:execute_params]) downstream_pipeline.tap do |pipeline| update_bridge_status!(@bridge, pipeline) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index dbe81521cfc..d3001e54288 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -27,6 +27,7 @@ module Ci Gitlab::Ci::Pipeline::Chain::Limit::JobActivity, Gitlab::Ci::Pipeline::Chain::CancelPendingPipelines, Gitlab::Ci::Pipeline::Chain::Metrics, + Gitlab::Ci::Pipeline::Chain::TemplateUsage, Gitlab::Ci::Pipeline::Chain::Pipeline::Process].freeze # Create a new pipeline in the specified project. @@ -81,7 +82,11 @@ module Ci .new(pipeline, command, SEQUENCE) .build! - schedule_head_pipeline_update if pipeline.persisted? + if pipeline.persisted? + schedule_head_pipeline_update + record_conversion_event + create_namespace_onboarding_action + end # If pipeline is not persisted, try to recover IID pipeline.reset_project_iid unless pipeline.persisted? @@ -116,6 +121,15 @@ module Ci end end + def record_conversion_event + Experiments::RecordConversionEventWorker.perform_async(:ci_syntax_templates, current_user.id) + Experiments::RecordConversionEventWorker.perform_async(:pipelines_empty_state, current_user.id) + end + + def create_namespace_onboarding_action + Namespaces::OnboardingPipelineCreatedWorker.perform_async(project.namespace_id) + end + def extra_options(content: nil, dry_run: false) { content: content, dry_run: dry_run } end diff --git a/app/services/ci/create_web_ide_terminal_service.rb b/app/services/ci/create_web_ide_terminal_service.rb index a78281aed16..785d82094b9 100644 --- a/app/services/ci/create_web_ide_terminal_service.rb +++ b/app/services/ci/create_web_ide_terminal_service.rb @@ -6,7 +6,7 @@ module Ci TerminalCreationError = Class.new(StandardError) - TERMINAL_NAME = 'terminal'.freeze + TERMINAL_NAME = 'terminal' attr_reader :terminal diff --git a/app/services/ci/destroy_expired_job_artifacts_service.rb b/app/services/ci/destroy_expired_job_artifacts_service.rb index 6e7caba8545..7d8a3c17abe 100644 --- a/app/services/ci/destroy_expired_job_artifacts_service.rb +++ b/app/services/ci/destroy_expired_job_artifacts_service.rb @@ -12,6 +12,10 @@ module Ci EXCLUSIVE_LOCK_KEY = 'expired_job_artifacts:destroy:lock' LOCK_TIMEOUT = 6.minutes + def initialize + @removed_artifacts_count = 0 + end + ## # Destroy expired job artifacts on GitLab instance # @@ -20,40 +24,22 @@ module Ci # which is scheduled every 7 minutes. def execute in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do - loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do - destroy_artifacts_batch - end + destroy_job_artifacts_with_slow_iteration(Time.current) end - end - - private - def destroy_artifacts_batch - destroy_job_artifacts_batch || destroy_pipeline_artifacts_batch + @removed_artifacts_count end - def destroy_job_artifacts_batch - artifacts = Ci::JobArtifact - .expired(BATCH_SIZE) - .unlocked - .with_destroy_preloads - .to_a - - return false if artifacts.empty? - - parallel_destroy_batch(artifacts) - true - end - - # TODO: Make sure this can also be parallelized - # https://gitlab.com/gitlab-org/gitlab/-/issues/270973 - def destroy_pipeline_artifacts_batch - artifacts = Ci::PipelineArtifact.expired(BATCH_SIZE).to_a - return false if artifacts.empty? + private - artifacts.each(&:destroy!) + def destroy_job_artifacts_with_slow_iteration(start_at) + Ci::JobArtifact.expired_before(start_at).each_batch(of: BATCH_SIZE, column: :expire_at, order: :desc) do |relation, index| + artifacts = relation.unlocked.with_destroy_preloads.to_a - true + parallel_destroy_batch(artifacts) if artifacts.any? + break if loop_timeout?(start_at) + break if index >= LOOP_LIMIT + end end def parallel_destroy_batch(job_artifacts) @@ -64,14 +50,14 @@ module Ci end # This is executed outside of the transaction because it depends on Redis - update_statistics_for(job_artifacts) - destroyed_artifacts_counter.increment({}, job_artifacts.size) + update_project_statistics_for(job_artifacts) + increment_monitoring_statistics(job_artifacts.size) end # This method is implemented in EE and it must do only database work def destroy_related_records_for(job_artifacts); end - def update_statistics_for(job_artifacts) + def update_project_statistics_for(job_artifacts) artifacts_by_project = job_artifacts.group_by(&:project) artifacts_by_project.each do |project, artifacts| delta = -artifacts.sum { |artifact| artifact.size.to_i } @@ -80,6 +66,11 @@ module Ci end end + def increment_monitoring_statistics(size) + destroyed_artifacts_counter.increment({}, size) + @removed_artifacts_count += size + end + def destroyed_artifacts_counter strong_memoize(:destroyed_artifacts_counter) do name = :destroyed_job_artifacts_count_total @@ -88,6 +79,10 @@ module Ci ::Gitlab::Metrics.counter(name, comment) end end + + def loop_timeout?(start_at) + Time.current > start_at + LOOP_TIMEOUT + end end end diff --git a/app/services/ci/pipelines/create_artifact_service.rb b/app/services/ci/pipeline_artifacts/coverage_report_service.rb index bfaf317241a..9f5c445c91a 100644 --- a/app/services/ci/pipelines/create_artifact_service.rb +++ b/app/services/ci/pipeline_artifacts/coverage_report_service.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Ci - module Pipelines - class CreateArtifactService + module PipelineArtifacts + class CoverageReportService def execute(pipeline) return unless pipeline.can_generate_coverage_reports? return if pipeline.has_coverage_reports? diff --git a/app/services/ci/pipeline_artifacts/destroy_expired_artifacts_service.rb b/app/services/ci/pipeline_artifacts/destroy_expired_artifacts_service.rb new file mode 100644 index 00000000000..0dbabe178da --- /dev/null +++ b/app/services/ci/pipeline_artifacts/destroy_expired_artifacts_service.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Ci + module PipelineArtifacts + class DestroyExpiredArtifactsService + include ::Gitlab::LoopHelpers + include ::Gitlab::Utils::StrongMemoize + + BATCH_SIZE = 100 + LOOP_TIMEOUT = 5.minutes + LOOP_LIMIT = 1000 + + def initialize + @removed_artifacts_count = 0 + end + + def execute + loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do + destroy_artifacts_batch + end + + @removed_artifacts_count + end + + private + + def destroy_artifacts_batch + artifacts = ::Ci::PipelineArtifact.expired(BATCH_SIZE).to_a + return false if artifacts.empty? + + artifacts.each(&:destroy!) + increment_stats(artifacts.size) + + true + end + + def increment_stats(size) + destroyed_artifacts_counter.increment({}, size) + @removed_artifacts_count += size + end + + def destroyed_artifacts_counter + strong_memoize(:destroyed_artifacts_counter) do + name = :destroyed_pipeline_artifacts_count_total + comment = 'Counter of destroyed expired pipeline artifacts' + + ::Gitlab::Metrics.counter(name, comment) + end + end + end + end +end diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb index d9f41b7040e..a31f5e9056e 100644 --- a/app/services/ci/pipeline_trigger_service.rb +++ b/app/services/ci/pipeline_trigger_service.rb @@ -21,10 +21,10 @@ module Ci # this check is to not leak the presence of the project if user cannot read it return unless trigger.project == project - pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref]) + pipeline = Ci::CreatePipelineService + .new(project, trigger.owner, ref: params[:ref], variables_attributes: variables) .execute(:trigger, ignore_skip_ci: true) do |pipeline| pipeline.trigger_requests.build(trigger: trigger) - pipeline.variables.build(variables) end if pipeline.persisted? @@ -44,7 +44,8 @@ module Ci # this check is to not leak the presence of the project if user cannot read it return unless can?(job.user, :read_project, project) - pipeline = Ci::CreatePipelineService.new(project, job.user, ref: params[:ref]) + pipeline = Ci::CreatePipelineService + .new(project, job.user, ref: params[:ref], variables_attributes: variables) .execute(:pipeline, ignore_skip_ci: true) do |pipeline| source = job.sourced_pipelines.build( source_pipeline: job.pipeline, @@ -53,7 +54,6 @@ module Ci project: project) pipeline.source_pipeline = source - pipeline.variables.build(variables) end if pipeline.persisted? diff --git a/app/services/ci/play_build_service.rb b/app/services/ci/play_build_service.rb index 6adeca624a8..ebc980a9053 100644 --- a/app/services/ci/play_build_service.rb +++ b/app/services/ci/play_build_service.rb @@ -5,6 +5,10 @@ module Ci def execute(build, job_variables_attributes = nil) raise Gitlab::Access::AccessDeniedError unless can?(current_user, :play_job, build) + if job_variables_attributes.present? && !can?(current_user, :set_pipeline_variables, project) + raise Gitlab::Access::AccessDeniedError + end + # Try to enqueue the build, otherwise create a duplicate. # if build.enqueue diff --git a/app/services/ci/process_build_service.rb b/app/services/ci/process_build_service.rb index 12cdca24066..dd7b562cdb7 100644 --- a/app/services/ci/process_build_service.rb +++ b/app/services/ci/process_build_service.rb @@ -26,6 +26,27 @@ module Ci end def valid_statuses_for_build(build) + if ::Feature.enabled?(:skip_dag_manual_and_delayed_jobs, default_enabled: :yaml) + current_valid_statuses_for_build(build) + else + legacy_valid_statuses_for_build(build) + end + end + + def current_valid_statuses_for_build(build) + case build.when + when 'on_success', 'manual', 'delayed' + build.scheduling_type_dag? ? %w[success] : %w[success skipped] + when 'on_failure' + %w[failed] + when 'always' + %w[success failed skipped] + else + [] + end + end + + def legacy_valid_statuses_for_build(build) case build.when when 'on_success' build.scheduling_type_dag? ? %w[success] : %w[success skipped] diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb index 04d620d1d38..59691fe4ef3 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -8,8 +8,8 @@ module Ci JOB_QUEUE_DURATION_SECONDS_BUCKETS = [1, 3, 10, 30, 60, 300, 900, 1800, 3600].freeze JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET = 5.freeze - METRICS_SHARD_TAG_PREFIX = 'metrics_shard::'.freeze - DEFAULT_METRICS_SHARD = 'default'.freeze + METRICS_SHARD_TAG_PREFIX = 'metrics_shard::' + DEFAULT_METRICS_SHARD = 'default' Result = Struct.new(:build, :build_json, :valid?) diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb index f397ada0696..e5e79f70616 100644 --- a/app/services/ci/retry_build_service.rb +++ b/app/services/ci/retry_build_service.rb @@ -2,6 +2,8 @@ module Ci class RetryBuildService < ::BaseService + include Gitlab::OptimisticLocking + def self.clone_accessors %i[pipeline project ref tag options name allow_failure stage stage_id stage_idx trigger_request @@ -65,8 +67,8 @@ module Ci end def mark_subsequent_stages_as_processable(build) - build.pipeline.processables.skipped.after_stage(build.stage_idx).find_each do |processable| - Gitlab::OptimisticLocking.retry_lock(processable, &:process) + build.pipeline.processables.skipped.after_stage(build.stage_idx).find_each do |skipped| + retry_optimistic_lock(skipped) { |build| build.process(current_user) } end end end diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb index 45244d16393..dea4bf73a4c 100644 --- a/app/services/ci/retry_pipeline_service.rb +++ b/app/services/ci/retry_pipeline_service.rb @@ -23,7 +23,7 @@ module Ci end pipeline.builds.latest.skipped.find_each do |skipped| - retry_optimistic_lock(skipped) { |build| build.process } + retry_optimistic_lock(skipped) { |build| build.process(current_user) } end pipeline.reset_ancestor_bridges! diff --git a/app/services/ci/test_failure_history_service.rb b/app/services/ci/test_failure_history_service.rb index 99a2592ec06..61fda79a4a2 100644 --- a/app/services/ci/test_failure_history_service.rb +++ b/app/services/ci/test_failure_history_service.rb @@ -30,7 +30,6 @@ module Ci end def should_track_failures? - return false unless Feature.enabled?(:test_failure_history, project) return false unless project.default_branch_or_master == pipeline.ref # We fetch for up to MAX_TRACKABLE_FAILURES + 1 builds. So if ever we get diff --git a/app/services/ci/update_build_state_service.rb b/app/services/ci/update_build_state_service.rb index f01d41d9414..874f4bf459a 100644 --- a/app/services/ci/update_build_state_service.rb +++ b/app/services/ci/update_build_state_service.rb @@ -111,7 +111,7 @@ module Ci Result.new(status: 200) when 'failed' - build.drop!(params[:failure_reason] || :unknown_failure) + build.drop_with_exit_code!(params[:failure_reason] || :unknown_failure, params[:exit_code]) Result.new(status: 200) else diff --git a/app/services/concerns/schedule_bulk_repository_shard_moves_methods.rb b/app/services/concerns/schedule_bulk_repository_shard_moves_methods.rb new file mode 100644 index 00000000000..eb03f3a9f3a --- /dev/null +++ b/app/services/concerns/schedule_bulk_repository_shard_moves_methods.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module ScheduleBulkRepositoryShardMovesMethods + extend ActiveSupport::Concern + include BaseServiceUtility + + class_methods do + def enqueue(source_storage_name, destination_storage_name = nil) + schedule_bulk_worker_klass.perform_async(source_storage_name, destination_storage_name) + end + + def schedule_bulk_worker_klass + raise NotImplementedError + end + end + + def execute(source_storage_name, destination_storage_name = nil) + shard = Shard.find_by_name!(source_storage_name) + + repository_klass.for_shard(shard).each_batch(column: container_column) do |relation| + container_klass.id_in(relation.select(container_column)).each do |container| + container.with_lock do + next if container.repository_storage != source_storage_name + + storage_move = container.repository_storage_moves.build( + source_storage_name: source_storage_name, + destination_storage_name: destination_storage_name + ) + + unless storage_move.schedule + log_info("Container #{container.full_path} (#{container.id}) was skipped: #{storage_move.errors.full_messages.to_sentence}") + end + end + end + end + + success + end + + private + + def repository_klass + raise NotImplementedError + end + + def container_klass + raise NotImplementedError + end + + def container_column + raise NotImplementedError + end +end diff --git a/app/services/concerns/update_repository_storage_methods.rb b/app/services/concerns/update_repository_storage_methods.rb new file mode 100644 index 00000000000..c3a55e9379e --- /dev/null +++ b/app/services/concerns/update_repository_storage_methods.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +module UpdateRepositoryStorageMethods + Error = Class.new(StandardError) + SameFilesystemError = Class.new(Error) + + attr_reader :repository_storage_move + delegate :container, :source_storage_name, :destination_storage_name, to: :repository_storage_move + + def initialize(repository_storage_move) + @repository_storage_move = repository_storage_move + end + + def execute + repository_storage_move.with_lock do + return ServiceResponse.success unless repository_storage_move.scheduled? # rubocop:disable Cop/AvoidReturnFromBlocks + + repository_storage_move.start! + end + + raise SameFilesystemError if same_filesystem?(source_storage_name, destination_storage_name) + + mirror_repositories + + repository_storage_move.transaction do + repository_storage_move.finish_replication! + + track_repository(destination_storage_name) + end + + remove_old_paths + enqueue_housekeeping + + repository_storage_move.finish_cleanup! + + ServiceResponse.success + rescue StandardError => e + repository_storage_move.do_fail! + + Gitlab::ErrorTracking.track_exception(e, container_klass: container.class.to_s, container_path: container.full_path) + + ServiceResponse.error( + message: s_("UpdateRepositoryStorage|Error moving repository storage for %{container_full_path} - %{message}") % { container_full_path: container.full_path, message: e.message } + ) + end + + private + + def track_repository(destination_shard) + raise NotImplementedError + end + + def mirror_repositories + raise NotImplementedError + end + + def mirror_repository(type:) + unless wait_for_pushes(type) + raise Error, s_('UpdateRepositoryStorage|Timeout waiting for %{type} repository pushes') % { type: type.name } + end + + repository = type.repository_for(container) + full_path = repository.full_path + raw_repository = repository.raw + checksum = repository.checksum + + # Initialize a git repository on the target path + new_repository = Gitlab::Git::Repository.new( + destination_storage_name, + raw_repository.relative_path, + raw_repository.gl_repository, + full_path + ) + + new_repository.replicate(raw_repository) + new_checksum = new_repository.checksum + + if checksum != new_checksum + raise Error, s_('UpdateRepositoryStorage|Failed to verify %{type} repository checksum from %{old} to %{new}') % { type: type.name, old: checksum, new: new_checksum } + end + end + + def same_filesystem?(old_storage, new_storage) + Gitlab::GitalyClient.filesystem_id(old_storage) == Gitlab::GitalyClient.filesystem_id(new_storage) + end + + def remove_old_paths + if container.repository_exists? + Gitlab::Git::Repository.new( + source_storage_name, + "#{container.disk_path}.git", + nil, + nil + ).remove + end + end + + def enqueue_housekeeping + # no-op + end + + def wait_for_pushes(type) + reference_counter = container.reference_counter(type: type) + + # Try for 30 seconds, polling every 10 + 3.times do + return true if reference_counter.value == 0 + + sleep 10 + end + + false + end +end diff --git a/app/services/container_expiration_policies/cleanup_service.rb b/app/services/container_expiration_policies/cleanup_service.rb index 4719c99af6d..b9e623e2e07 100644 --- a/app/services/container_expiration_policies/cleanup_service.rb +++ b/app/services/container_expiration_policies/cleanup_service.rb @@ -4,6 +4,8 @@ module ContainerExpirationPolicies class CleanupService attr_reader :repository + SERVICE_RESULT_FIELDS = %i[original_size before_truncate_size after_truncate_size before_delete_size].freeze + def initialize(repository) @repository = repository end @@ -13,28 +15,37 @@ module ContainerExpirationPolicies repository.start_expiration_policy! - result = Projects::ContainerRepository::CleanupTagsService + service_result = Projects::ContainerRepository::CleanupTagsService .new(project, nil, policy_params.merge('container_expiration_policy' => true)) .execute(repository) - if result[:status] == :success + if service_result[:status] == :success repository.update!( expiration_policy_cleanup_status: :cleanup_unscheduled, expiration_policy_started_at: nil, expiration_policy_completed_at: Time.zone.now ) - success(:finished) + success(:finished, service_result) else repository.cleanup_unfinished! - success(:unfinished) + success(:unfinished, service_result) end end private - def success(cleanup_status) - ServiceResponse.success(message: "cleanup #{cleanup_status}", payload: { cleanup_status: cleanup_status, container_repository_id: repository.id }) + def success(cleanup_status, service_result) + payload = { + cleanup_status: cleanup_status, + container_repository_id: repository.id + } + + SERVICE_RESULT_FIELDS.each do |field| + payload["cleanup_tags_service_#{field}".to_sym] = service_result[field] + end + + ServiceResponse.success(message: "cleanup #{cleanup_status}", payload: payload) end def policy_params diff --git a/app/services/draft_notes/base_service.rb b/app/services/draft_notes/base_service.rb index 89daae0e8f4..95c291ea800 100644 --- a/app/services/draft_notes/base_service.rb +++ b/app/services/draft_notes/base_service.rb @@ -8,6 +8,10 @@ module DraftNotes @merge_request, @current_user, @params = merge_request, current_user, params.dup end + def merge_request_activity_counter + Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter + end + private def draft_notes diff --git a/app/services/draft_notes/create_service.rb b/app/services/draft_notes/create_service.rb index 501778b7d5f..5ff971b66c1 100644 --- a/app/services/draft_notes/create_service.rb +++ b/app/services/draft_notes/create_service.rb @@ -31,6 +31,10 @@ module DraftNotes merge_request.diffs.clear_cache end + if draft_note.persisted? + merge_request_activity_counter.track_create_review_note_action(user: current_user) + end + draft_note end diff --git a/app/services/draft_notes/publish_service.rb b/app/services/draft_notes/publish_service.rb index a9a7304e5ed..316abff4552 100644 --- a/app/services/draft_notes/publish_service.rb +++ b/app/services/draft_notes/publish_service.rb @@ -9,6 +9,7 @@ module DraftNotes publish_draft_note(draft) else publish_draft_notes + merge_request_activity_counter.track_publish_review_action(user: current_user) end success diff --git a/app/services/feature_flags/base_service.rb b/app/services/feature_flags/base_service.rb index 9b27df90992..c11c465252e 100644 --- a/app/services/feature_flags/base_service.rb +++ b/app/services/feature_flags/base_service.rb @@ -6,6 +6,11 @@ module FeatureFlags AUDITABLE_ATTRIBUTES = %w(name description active).freeze + def success(**args) + sync_to_jira(args[:feature_flag]) + super + end + protected def audit_event(feature_flag) @@ -34,6 +39,16 @@ module FeatureFlags audit_event.security_event end + def sync_to_jira(feature_flag) + return unless feature_flag.present? + return unless Feature.enabled?(:jira_sync_feature_flags, feature_flag.project) + + seq_id = ::Atlassian::JiraConnect::Client.generate_update_sequence_id + feature_flag.run_after_commit do + ::JiraConnect::SyncFeatureFlagsWorker.perform_async(feature_flag.id, seq_id) + end + end + def created_scope_message(scope) "Created rule <strong>#{scope.environment_scope}</strong> "\ "and set it as <strong>#{scope.active ? "active" : "inactive"}</strong> "\ diff --git a/app/services/git/branch_push_service.rb b/app/services/git/branch_push_service.rb index 2ec6ac99ece..d250bca7bf2 100644 --- a/app/services/git/branch_push_service.rb +++ b/app/services/git/branch_push_service.rb @@ -72,10 +72,10 @@ module Git end def perform_housekeeping - housekeeping = Projects::HousekeepingService.new(project) + housekeeping = Repositories::HousekeepingService.new(project) housekeeping.increment! housekeeping.execute if housekeeping.needed? - rescue Projects::HousekeepingService::LeaseTaken + rescue Repositories::HousekeepingService::LeaseTaken end def removing_branch? diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index 52600f5b88f..06a3b31c665 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -35,6 +35,7 @@ module Groups @group.add_owner(current_user) @group.create_namespace_settings Service.create_from_active_default_integrations(@group, :group_id) + OnboardingProgress.onboard(@group) end end diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb index 1bff70e6c2e..c7107e2fa56 100644 --- a/app/services/groups/destroy_service.rb +++ b/app/services/groups/destroy_service.rb @@ -29,17 +29,32 @@ module Groups group.chat_team&.remove_mattermost_team(current_user) - user_ids_for_project_authorizations_refresh = group.user_ids_for_project_authorizations + # If any other groups are shared with the group that is being destroyed, + # we should specifically trigger update of all project authorizations + # for users that are the members of this group. + # If not, the project authorization records of these users to projects within the shared groups + # will never be removed, causing inconsistencies with access permissions. + if any_other_groups_are_shared_with_this_group? + user_ids_for_project_authorizations_refresh = group.user_ids_for_project_authorizations + end group.destroy - UserProjectAccessChangedService - .new(user_ids_for_project_authorizations_refresh) - .execute(blocking: true) + if user_ids_for_project_authorizations_refresh.present? + UserProjectAccessChangedService + .new(user_ids_for_project_authorizations_refresh) + .execute(blocking: true) + end group end # rubocop: enable CodeReuse/ActiveRecord + + private + + def any_other_groups_are_shared_with_this_group? + group.shared_group_links.any? + end end end diff --git a/app/services/ide/base_config_service.rb b/app/services/ide/base_config_service.rb index 1f8d5c17584..0501fab53af 100644 --- a/app/services/ide/base_config_service.rb +++ b/app/services/ide/base_config_service.rb @@ -4,7 +4,7 @@ module Ide class BaseConfigService < ::BaseService ValidationError = Class.new(StandardError) - WEBIDE_CONFIG_FILE = '.gitlab/.gitlab-webide.yml'.freeze + WEBIDE_CONFIG_FILE = '.gitlab/.gitlab-webide.yml' attr_reader :config, :config_content diff --git a/app/services/incident_management/pager_duty/process_webhook_service.rb b/app/services/incident_management/pager_duty/process_webhook_service.rb index 027425e4aaa..ccbca671b37 100644 --- a/app/services/incident_management/pager_duty/process_webhook_service.rb +++ b/app/services/incident_management/pager_duty/process_webhook_service.rb @@ -2,7 +2,7 @@ module IncidentManagement module PagerDuty - class ProcessWebhookService < BaseService + class ProcessWebhookService include Gitlab::Utils::StrongMemoize include IncidentManagement::Settings @@ -12,6 +12,11 @@ module IncidentManagement # https://developer.pagerduty.com/docs/webhooks/v2-overview/#webhook-types PAGER_DUTY_PROCESSABLE_EVENT_TYPES = %w(incident.trigger).freeze + def initialize(project, payload) + @project = project + @payload = payload + end + def execute(token) return forbidden unless webhook_setting_active? return unauthorized unless valid_token?(token) @@ -24,6 +29,8 @@ module IncidentManagement private + attr_reader :project, :payload + def process_incidents pager_duty_processable_events.each do |event| ::IncidentManagement::PagerDuty::ProcessIncidentWorker.perform_async(project.id, event['incident']) @@ -33,7 +40,7 @@ module IncidentManagement def pager_duty_processable_events strong_memoize(:pager_duty_processable_events) do ::PagerDuty::WebhookPayloadParser - .call(params.to_h) + .call(payload.to_h) .filter { |msg| msg['event'].to_s.in?(PAGER_DUTY_PROCESSABLE_EVENT_TYPES) } end end @@ -47,7 +54,7 @@ module IncidentManagement end def valid_payload_size? - Gitlab::Utils::DeepSize.new(params, max_size: PAGER_DUTY_PAYLOAD_SIZE_LIMIT).valid? + Gitlab::Utils::DeepSize.new(payload, max_size: PAGER_DUTY_PAYLOAD_SIZE_LIMIT).valid? end def accepted diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb index 79be771b3fb..d3d543edcd7 100644 --- a/app/services/issuable/bulk_update_service.rb +++ b/app/services/issuable/bulk_update_service.rb @@ -34,6 +34,8 @@ module Issuable def permitted_attrs(type) attrs = %i(state_event milestone_id add_label_ids remove_label_ids subscription_event) + attrs.push(:sprint_id) if type == 'issue' + if type == 'issue' || type == 'merge_request' attrs.push(:assignee_ids) else diff --git a/app/services/issuable/export_csv/base_service.rb b/app/services/issuable/export_csv/base_service.rb new file mode 100644 index 00000000000..49ff05935c9 --- /dev/null +++ b/app/services/issuable/export_csv/base_service.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Issuable + module ExportCsv + class BaseService + # Target attachment size before base64 encoding + TARGET_FILESIZE = 15.megabytes + + def initialize(issuables_relation, project) + @issuables = issuables_relation + @project = project + end + + def csv_data + csv_builder.render(TARGET_FILESIZE) + end + + private + + attr_reader :project, :issuables + + # rubocop: disable CodeReuse/ActiveRecord + def csv_builder + @csv_builder ||= + CsvBuilder.new(issuables.preload(associations_to_preload), header_to_value_hash) + end + # rubocop: enable CodeReuse/ActiveRecord + + def associations_to_preload + [] + end + + def header_to_value_hash + raise NotImplementedError + end + end + end +end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 60e5293e218..6d41d449683 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -158,7 +158,9 @@ class IssuableBaseService < BaseService after_create(issuable) execute_hooks(issuable) - invalidate_cache_counts(issuable, users: issuable.assignees) + + users_to_invalidate = issuable.allows_reviewers? ? issuable.assignees | issuable.reviewers : issuable.assignees + invalidate_cache_counts(issuable, users: users_to_invalidate) issuable.update_project_counter_caches end diff --git a/app/services/issues/clone_service.rb b/app/services/issues/clone_service.rb index 789da312958..4c9c34f1247 100644 --- a/app/services/issues/clone_service.rb +++ b/app/services/issues/clone_service.rb @@ -88,3 +88,5 @@ module Issues end end end + +Issues::CloneService.prepend_if_ee('EE::Issues::CloneService') diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb index c3677de015f..baf7974c45d 100644 --- a/app/services/issues/close_service.rb +++ b/app/services/issues/close_service.rb @@ -31,7 +31,7 @@ module Issues closed_via = _("commit %{commit_id}") % { commit_id: closed_via.id } if closed_via.is_a?(Commit) - notification_service.async.close_issue(issue, current_user, closed_via: closed_via) if notifications + notification_service.async.close_issue(issue, current_user, { closed_via: closed_via }) if notifications todo_service.close_issue(issue, current_user) resolve_alert(issue) execute_hooks(issue, 'close') @@ -39,7 +39,10 @@ module Issues issue.update_project_counter_caches track_incident_action(current_user, issue, :incident_closed) - store_first_mentioned_in_commit_at(issue, closed_via) if closed_via.is_a?(MergeRequest) + if closed_via.is_a?(MergeRequest) + store_first_mentioned_in_commit_at(issue, closed_via) + OnboardingProgressService.new(project.namespace).execute(action: :issue_auto_closed) + end delete_milestone_closed_issue_counter_cache(issue.milestone) end diff --git a/app/services/issues/export_csv_service.rb b/app/services/issues/export_csv_service.rb index 8f513632929..2181c46c90d 100644 --- a/app/services/issues/export_csv_service.rb +++ b/app/services/issues/export_csv_service.rb @@ -1,36 +1,14 @@ # frozen_string_literal: true module Issues - class ExportCsvService + class ExportCsvService < Issuable::ExportCsv::BaseService include Gitlab::Routing.url_helpers include GitlabRoutingHelper - # Target attachment size before base64 encoding - TARGET_FILESIZE = 15000000 - - attr_reader :project - - def initialize(issues_relation, project) - @issues = issues_relation - @labels = @issues.labels_hash - @project = project - end - - def csv_data - csv_builder.render(TARGET_FILESIZE) - end - def email(user) Notify.issues_csv_email(user, project, csv_data, csv_builder.status).deliver_now end - # rubocop: disable CodeReuse/ActiveRecord - def csv_builder - @csv_builder ||= - CsvBuilder.new(@issues.preload(associations_to_preload), header_to_value_hash) - end - # rubocop: enable CodeReuse/ActiveRecord - private def associations_to_preload @@ -63,7 +41,7 @@ module Issues end def issue_labels(issue) - @labels[issue.id].sort.join(',').presence + issuables.labels_hash[issue.id].sort.join(',').presence end # rubocop: disable CodeReuse/ActiveRecord diff --git a/app/services/jira_connect/sync_service.rb b/app/services/jira_connect/sync_service.rb index b2af284f1f0..bddc7cbe5a0 100644 --- a/app/services/jira_connect/sync_service.rb +++ b/app/services/jira_connect/sync_service.rb @@ -31,7 +31,7 @@ module JiraConnect jira_response: response&.to_json } - if response && (response['errorMessages'] || response['rejectedBuilds'].present?) + if response && response['errorMessages'].present? logger.error(message) else logger.info(message) diff --git a/app/services/jira_connect_subscriptions/create_service.rb b/app/services/jira_connect_subscriptions/create_service.rb index b169d97615d..38e5fe7e690 100644 --- a/app/services/jira_connect_subscriptions/create_service.rb +++ b/app/services/jira_connect_subscriptions/create_service.rb @@ -35,8 +35,6 @@ module JiraConnectSubscriptions end def schedule_sync_project_jobs - return unless Feature.enabled?(:jira_connect_full_namespace_sync) - namespace.all_projects.each_batch(of: MERGE_REQUEST_SYNC_BATCH_SIZE) do |projects, index| JiraConnect::SyncProjectWorker.bulk_perform_in_with_contexts( index * MERGE_REQUEST_SYNC_BATCH_DELAY, diff --git a/app/services/labels/create_service.rb b/app/services/labels/create_service.rb index a5b30e29e55..665d1035b2b 100644 --- a/app/services/labels/create_service.rb +++ b/app/services/labels/create_service.rb @@ -25,3 +25,5 @@ module Labels end end end + +Labels::CreateService.prepend_if_ee('EE::Labels::CreateService') diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb index 3588cda180f..5fcf2d711b0 100644 --- a/app/services/members/create_service.rb +++ b/app/services/members/create_service.rb @@ -54,7 +54,8 @@ module Members end def enqueue_onboarding_progress_action(source) - Namespaces::OnboardingUserAddedWorker.perform_async(source.id) + namespace_id = source.is_a?(Project) ? source.namespace_id : source.id + Namespaces::OnboardingUserAddedWorker.perform_async(namespace_id) end end end diff --git a/app/services/merge_requests/after_create_service.rb b/app/services/merge_requests/after_create_service.rb index fbb9d5fa9dc..03fcb5a4c1b 100644 --- a/app/services/merge_requests/after_create_service.rb +++ b/app/services/merge_requests/after_create_service.rb @@ -4,6 +4,7 @@ module MergeRequests class AfterCreateService < MergeRequests::BaseService def execute(merge_request) event_service.open_mr(merge_request, current_user) + merge_request_activity_counter.track_create_mr_action(user: current_user) notification_service.new_merge_request(merge_request, current_user) create_pipeline_for(merge_request, current_user) @@ -12,7 +13,7 @@ module MergeRequests merge_request.diffs(include_stats: false).write_cache merge_request.create_cross_references!(current_user) - NamespaceOnboardingAction.create_action(merge_request.target_project.namespace, :merge_request_created) + OnboardingProgressService.new(merge_request.target_project.namespace).execute(action: :merge_request_created) end end end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index 265b211066e..0613c061f2e 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -52,6 +52,10 @@ module MergeRequests "#<#{self.class} #{merge_request.to_reference(full: true)}>" end + def merge_request_activity_counter + Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter + end + private def enqueue_jira_connect_messages_for(merge_request) diff --git a/app/services/merge_requests/cleanup_refs_service.rb b/app/services/merge_requests/cleanup_refs_service.rb index 23ac8e393f4..2094ea00160 100644 --- a/app/services/merge_requests/cleanup_refs_service.rb +++ b/app/services/merge_requests/cleanup_refs_service.rb @@ -36,6 +36,8 @@ module MergeRequests return error('Failed to update schedule.') unless update_schedule success + rescue Gitlab::Git::Repository::GitError, Gitlab::Git::CommandError => e + error(e.message) end private diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb index b0a7face594..f83b14c7269 100644 --- a/app/services/merge_requests/close_service.rb +++ b/app/services/merge_requests/close_service.rb @@ -13,11 +13,12 @@ module MergeRequests if merge_request.close create_event(merge_request) + merge_request_activity_counter.track_close_mr_action(user: current_user) create_note(merge_request) notification_service.async.close_mr(merge_request, current_user) todo_service.close_merge_request(merge_request, current_user) execute_hooks(merge_request, 'close') - invalidate_cache_counts(merge_request, users: merge_request.assignees) + invalidate_cache_counts(merge_request, users: merge_request.assignees | merge_request.reviewers) merge_request.update_project_counter_caches cleanup_environments(merge_request) abort_auto_merge(merge_request, 'merge request was closed') diff --git a/app/services/merge_requests/create_from_issue_service.rb b/app/services/merge_requests/create_from_issue_service.rb index 95fb99d3e7a..78b462174c9 100644 --- a/app/services/merge_requests/create_from_issue_service.rb +++ b/app/services/merge_requests/create_from_issue_service.rb @@ -85,7 +85,8 @@ module MergeRequests source_project_id: target_project.id, source_branch: branch_name, target_project_id: target_project.id, - target_branch: target_branch + target_branch: target_branch, + assignee_ids: [current_user.id] } end diff --git a/app/services/merge_requests/export_csv_service.rb b/app/services/merge_requests/export_csv_service.rb index 1e7f0c8e722..8f2a70575e5 100644 --- a/app/services/merge_requests/export_csv_service.rb +++ b/app/services/merge_requests/export_csv_service.rb @@ -1,32 +1,16 @@ # frozen_string_literal: true module MergeRequests - class ExportCsvService + class ExportCsvService < Issuable::ExportCsv::BaseService include Gitlab::Routing.url_helpers include GitlabRoutingHelper - # Target attachment size before base64 encoding - TARGET_FILESIZE = 15.megabytes - - def initialize(merge_requests, project) - @project = project - @merge_requests = merge_requests - end - - def csv_data - csv_builder.render(TARGET_FILESIZE) - end - def email(user) - Notify.merge_requests_csv_email(user, @project, csv_data, csv_builder.status).deliver_now + Notify.merge_requests_csv_email(user, project, csv_data, csv_builder.status).deliver_now end private - def csv_builder - @csv_builder ||= CsvBuilder.new(@merge_requests.with_csv_entity_associations, header_to_value_hash) - end - def header_to_value_hash { 'MR IID' => 'iid', diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index ba22b458777..f4454db0af8 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -88,7 +88,9 @@ module MergeRequests end def try_merge - repository.merge(current_user, source, merge_request, commit_message) + repository.merge(current_user, source, merge_request, commit_message).tap do + merge_request.update_column(:squash_commit_sha, source) if merge_request.squash_on_merge? + end rescue Gitlab::Git::PreReceiveError => e raise MergeError, "Something went wrong during merge pre-receive hook. #{e.message}".strip diff --git a/app/services/merge_requests/mergeability_check_service.rb b/app/services/merge_requests/mergeability_check_service.rb index 627c747203c..96a2322f6a0 100644 --- a/app/services/merge_requests/mergeability_check_service.rb +++ b/app/services/merge_requests/mergeability_check_service.rb @@ -38,7 +38,6 @@ module MergeRequests # error otherwise. def execute(recheck: false, retry_lease: true) return service_error if service_error - return check_mergeability(recheck) unless merge_ref_auto_sync_lock_enabled? in_write_lock(retry_lease: retry_lease) do |retried| # When multiple calls are waiting for the same lock (retry_lease), @@ -64,10 +63,6 @@ module MergeRequests return ServiceResponse.error(message: 'Merge request is not mergeable') end - unless merge_ref_auto_sync_enabled? - return ServiceResponse.error(message: 'Merge ref is outdated due to disabled feature') - end - unless payload.fetch(:merge_ref_head) return ServiceResponse.error(message: 'Merge ref cannot be updated') end @@ -142,7 +137,6 @@ module MergeRequests # # Returns true if the merge-ref does not exists or is out of sync. def outdated_merge_ref? - return false unless merge_ref_auto_sync_enabled? return false unless merge_request.open? return true unless ref_head = merge_request.merge_ref_head @@ -157,21 +151,11 @@ module MergeRequests end def merge_to_ref - return true unless merge_ref_auto_sync_enabled? - params = { allow_conflicts: Feature.enabled?(:display_merge_conflicts_in_diff, project) } result = MergeRequests::MergeToRefService.new(project, merge_request.author, params).execute(merge_request) result[:status] == :success end - def merge_ref_auto_sync_enabled? - Feature.enabled?(:merge_ref_auto_sync, project, default_enabled: true) - end - - def merge_ref_auto_sync_lock_enabled? - Feature.enabled?(:merge_ref_auto_sync_lock, project, default_enabled: true) - end - def service_error strong_memoize(:service_error) do if !merge_request diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb index 1c78fca3c26..f04ec3c3e80 100644 --- a/app/services/merge_requests/post_merge_service.rb +++ b/app/services/merge_requests/post_merge_service.rb @@ -15,9 +15,10 @@ module MergeRequests todo_service.merge_merge_request(merge_request, current_user) create_event(merge_request) create_note(merge_request) + merge_request_activity_counter.track_merge_mr_action(user: current_user) notification_service.merge_mr(merge_request, current_user) execute_hooks(merge_request, 'merge') - invalidate_cache_counts(merge_request, users: merge_request.assignees) + invalidate_cache_counts(merge_request, users: merge_request.assignees | merge_request.reviewers) merge_request.update_project_counter_caches delete_non_latest_diffs(merge_request) cancel_review_app_jobs!(merge_request) diff --git a/app/services/merge_requests/reopen_service.rb b/app/services/merge_requests/reopen_service.rb index bcedbc61c65..35c50d63da0 100644 --- a/app/services/merge_requests/reopen_service.rb +++ b/app/services/merge_requests/reopen_service.rb @@ -8,11 +8,12 @@ module MergeRequests if merge_request.reopen create_event(merge_request) create_note(merge_request, 'reopened') + merge_request_activity_counter.track_reopen_mr_action(user: current_user) notification_service.async.reopen_mr(merge_request, current_user) execute_hooks(merge_request, 'reopen') merge_request.reload_diff(current_user) merge_request.mark_as_unchecked - invalidate_cache_counts(merge_request, users: merge_request.assignees) + invalidate_cache_counts(merge_request, users: merge_request.assignees | merge_request.reviewers) merge_request.update_project_counter_caches merge_request.cache_merge_request_closes_issues!(current_user) merge_request.cleanup_schedule&.destroy diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index bff7a43dd7b..d2e5a2a1619 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -112,9 +112,11 @@ module MergeRequests end def handle_reviewers_change(merge_request, old_reviewers) + affected_reviewers = (old_reviewers + merge_request.reviewers) - (old_reviewers & merge_request.reviewers) create_reviewer_note(merge_request, old_reviewers) notification_service.async.changed_reviewer_of_merge_request(merge_request, current_user, old_reviewers) todo_service.reassigned_reviewable(merge_request, current_user, old_reviewers) + invalidate_cache_counts(merge_request, users: affected_reviewers.compact) end def create_branch_change_note(issuable, branch_type, old_branch, new_branch) @@ -126,27 +128,29 @@ module MergeRequests override :handle_quick_actions def handle_quick_actions(merge_request) super + + # Ensure this parameter does not get used as an attribute + rebase = params.delete(:rebase) + + if rebase + rebase_from_quick_action(merge_request) + # Ignore "/merge" if "/rebase" is used to avoid an unexpected race + params.delete(:merge) + end + merge_from_quick_action(merge_request) if params[:merge] end + def rebase_from_quick_action(merge_request) + merge_request.rebase_async(current_user.id) + end + def merge_from_quick_action(merge_request) last_diff_sha = params.delete(:merge) - if Feature.enabled?(:merge_orchestration_service, merge_request.project, default_enabled: true) - MergeRequests::MergeOrchestrationService - .new(project, current_user, { sha: last_diff_sha }) - .execute(merge_request) - else - return unless merge_request.mergeable_with_quick_action?(current_user, last_diff_sha: last_diff_sha) - - merge_request.update(merge_error: nil) - - if merge_request.head_pipeline_active? - AutoMergeService.new(project, current_user, { sha: last_diff_sha }).execute(merge_request, AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS) - else - merge_request.merge_async(current_user.id, { sha: last_diff_sha }) - end - end + MergeRequests::MergeOrchestrationService + .new(project, current_user, { sha: last_diff_sha }) + .execute(merge_request) end override :quick_action_options diff --git a/app/services/namespaces/package_settings/update_service.rb b/app/services/namespaces/package_settings/update_service.rb new file mode 100644 index 00000000000..0964963647a --- /dev/null +++ b/app/services/namespaces/package_settings/update_service.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Namespaces + module PackageSettings + class UpdateService < BaseContainerService + include Gitlab::Utils::StrongMemoize + + ALLOWED_ATTRIBUTES = %i[maven_duplicates_allowed maven_duplicate_exception_regex].freeze + + def execute + return ServiceResponse.error(message: 'Access Denied', http_status: 403) unless allowed? + + if package_settings.update(package_settings_params) + ServiceResponse.success(payload: { package_settings: package_settings }) + else + ServiceResponse.error( + message: package_settings.errors.full_messages.to_sentence || 'Bad request', + http_status: 400 + ) + end + end + + private + + def package_settings + strong_memoize(:package_settings) do + @container.package_settings + end + end + + def allowed? + Ability.allowed?(current_user, :create_package_settings, @container) + end + + def package_settings_params + @params.slice(*ALLOWED_ATTRIBUTES) + end + end + end +end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index 9fffb6c372b..04b7fba207b 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -75,6 +75,7 @@ module Notes end track_note_creation_usage_for_issues(note) if note.for_issue? + track_note_creation_usage_for_merge_requests(note) if note.for_merge_request? end def do_commands(note, update_params, message, only_commands) @@ -119,5 +120,9 @@ module Notes def track_note_creation_usage_for_issues(note) Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_comment_added_action(author: note.author) end + + def track_note_creation_usage_for_merge_requests(note) + Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter.track_create_comment_action(note: note) + end end end diff --git a/app/services/notes/destroy_service.rb b/app/services/notes/destroy_service.rb index 2b6ec47eaef..85f54a39add 100644 --- a/app/services/notes/destroy_service.rb +++ b/app/services/notes/destroy_service.rb @@ -9,6 +9,7 @@ module Notes clear_noteable_diffs_cache(note) track_note_removal_usage_for_issues(note) if note.for_issue? + track_note_removal_usage_for_merge_requests(note) if note.for_merge_request? end private @@ -16,6 +17,10 @@ module Notes def track_note_removal_usage_for_issues(note) Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_comment_removed_action(author: note.author) end + + def track_note_removal_usage_for_merge_requests(note) + Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter.track_remove_comment_action(note: note) + end end end diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb index 37872f7fbdb..857ffbb6965 100644 --- a/app/services/notes/update_service.rb +++ b/app/services/notes/update_service.rb @@ -15,6 +15,7 @@ module Notes end track_note_edit_usage_for_issues(note) if note.for_issue? + track_note_edit_usage_for_merge_requests(note) if note.for_merge_request? only_commands = false @@ -95,6 +96,10 @@ module Notes def track_note_edit_usage_for_issues(note) Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_comment_edited_action(author: note.author) end + + def track_note_edit_usage_for_merge_requests(note) + Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter.track_edit_comment_action(note: note) + end end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 4ff462191fe..5a71e0eac7c 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -118,8 +118,8 @@ class NotificationService # * project team members with notification level higher then Participating # * users with custom level checked with "close issue" # - def close_issue(issue, current_user, closed_via: nil) - close_resource_email(issue, current_user, :closed_issue_email, closed_via: closed_via) + def close_issue(issue, current_user, params = {}) + close_resource_email(issue, current_user, :closed_issue_email, closed_via: params[:closed_via]) end # When we reassign an issue we should send an email to: @@ -481,6 +481,12 @@ class NotificationService mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later end + def updated_group_member_expiration(group_member) + return true unless group_member.notifiable?(:mention) + + mailer.member_expiration_date_updated_email(group_member.real_source_type, group_member.id).deliver_later + end + def project_was_moved(project, old_path_with_namespace) recipients = project_moved_recipients(project) recipients = notifiable_users(recipients, :custom, custom_action: :moved_project, project: project) diff --git a/app/services/onboarding_progress_service.rb b/app/services/onboarding_progress_service.rb index ebe7caabdef..241bd8a01ca 100644 --- a/app/services/onboarding_progress_service.rb +++ b/app/services/onboarding_progress_service.rb @@ -2,10 +2,12 @@ class OnboardingProgressService def initialize(namespace) - @namespace = namespace.root_ancestor + @namespace = namespace&.root_ancestor end def execute(action:) - NamespaceOnboardingAction.create_action(@namespace, action) + return unless @namespace + + OnboardingProgress.register(@namespace, action) end end diff --git a/app/services/packages/create_event_service.rb b/app/services/packages/create_event_service.rb index f0328ceb08a..63248ef07c9 100644 --- a/app/services/packages/create_event_service.rb +++ b/app/services/packages/create_event_service.rb @@ -3,11 +3,13 @@ module Packages class CreateEventService < BaseService def execute - if Feature.enabled?(:collect_package_events_redis) && redis_event_name - if guest? - ::Gitlab::UsageDataCounters::GuestPackageEventCounter.count(redis_event_name) - else - ::Gitlab::UsageDataCounters::HLLRedisCounter.track_event(current_user.id, redis_event_name) + if Feature.enabled?(:collect_package_events_redis, default_enabled: true) + ::Packages::Event.unique_counters_for(event_scope, event_name, originator_type).each do |event_name| + ::Gitlab::UsageDataCounters::HLLRedisCounter.track_event(event_name, values: current_user.id) + end + + ::Packages::Event.counters_for(event_scope, event_name, originator_type).each do |event_name| + ::Gitlab::UsageDataCounters::PackageEventCounter.count(event_name) end end @@ -23,10 +25,6 @@ module Packages private - def redis_event_name - @redis_event_name ||= ::Packages::Event.allowed_event_name(event_scope, event_name, originator_type) - end - def event_scope @event_scope ||= scope.is_a?(::Packages::Package) ? scope.package_type : scope end diff --git a/app/services/packages/debian/create_package_file_service.rb b/app/services/packages/debian/create_package_file_service.rb new file mode 100644 index 00000000000..2022a63a725 --- /dev/null +++ b/app/services/packages/debian/create_package_file_service.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Packages + module Debian + class CreatePackageFileService + def initialize(package, params) + @package = package + @params = params + end + + def execute + raise ArgumentError, "Invalid package" unless package.present? + + # Debian package file are first uploaded to incoming with empty metadata, + # and are moved later by Packages::Debian::ProcessChangesService + package.package_files.create!( + file: params[:file], + size: params[:file]&.size, + file_name: params[:file_name], + file_sha1: params[:file_sha1], + file_sha256: params[:file]&.sha256, + file_md5: params[:file_md5], + debian_file_metadatum_attributes: { + file_type: 'unknown', + architecture: nil, + fields: nil + } + ) + end + + private + + attr_reader :package, :params + end + end +end diff --git a/app/services/packages/debian/extract_metadata_service.rb b/app/services/packages/debian/extract_metadata_service.rb new file mode 100644 index 00000000000..fd5832bc0ba --- /dev/null +++ b/app/services/packages/debian/extract_metadata_service.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +module Packages + module Debian + class ExtractMetadataService + include Gitlab::Utils::StrongMemoize + + ExtractionError = Class.new(StandardError) + + def initialize(package_file) + @package_file = package_file + end + + def execute + raise ExtractionError.new('invalid package file') unless valid_package_file? + + extract_metadata + end + + private + + attr_reader :package_file + + def valid_package_file? + package_file && + package_file.package&.debian? && + package_file.file.size > 0 # rubocop:disable Style/ZeroLengthPredicate + end + + def file_type_basic + %i[dsc deb udeb buildinfo changes].each do |format| + return format if package_file.file_name.end_with?(".#{format}") + end + + nil + end + + def file_type_source + # https://manpages.debian.org/buster/dpkg-dev/dpkg-source.1.en.html + %i[gzip bzip2 lzma xz].each do |format| + return :source if package_file.file_name.end_with?(".tar.#{format}") + end + + nil + end + + def file_type + strong_memoize(:file_type) do + file_type_basic || file_type_source || :unknown + end + end + + def file_type_debian? + file_type == :deb || file_type == :udeb + end + + def file_type_meta? + file_type == :dsc || file_type == :buildinfo || file_type == :changes + end + + def extracted_fields + if file_type_debian? + package_file.file.use_file do |file_path| + ::Packages::Debian::ExtractDebMetadataService.new(file_path).execute + end + elsif file_type_meta? + package_file.file.use_file do |file_path| + ::Packages::Debian::ParseDebian822Service.new(File.read(file_path)).execute.each_value.first + end + end + end + + def extract_metadata + fields = extracted_fields + architecture = fields.delete(:Architecture) if file_type_debian? + + { + file_type: file_type, + architecture: architecture, + fields: fields + } + end + end + end +end diff --git a/app/services/packages/debian/get_or_create_incoming_service.rb b/app/services/packages/debian/get_or_create_incoming_service.rb new file mode 100644 index 00000000000..09e7877a2b4 --- /dev/null +++ b/app/services/packages/debian/get_or_create_incoming_service.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Packages + module Debian + class GetOrCreateIncomingService < ::Packages::CreatePackageService + def execute + find_or_create_package!(:debian, name: 'incoming', version: nil) + end + end + end +end diff --git a/app/services/packages/maven/find_or_create_package_service.rb b/app/services/packages/maven/find_or_create_package_service.rb index f598b5e7cd4..8ee449cbfdc 100644 --- a/app/services/packages/maven/find_or_create_package_service.rb +++ b/app/services/packages/maven/find_or_create_package_service.rb @@ -2,14 +2,23 @@ module Packages module Maven class FindOrCreatePackageService < BaseService - MAVEN_METADATA_FILE = 'maven-metadata.xml'.freeze - SNAPSHOT_TERM = '-SNAPSHOT'.freeze + MAVEN_METADATA_FILE = 'maven-metadata.xml' + SNAPSHOT_TERM = '-SNAPSHOT' def execute package = ::Packages::Maven::PackageFinder.new(params[:path], current_user, project: project) .execute + unless Namespace::PackageSetting.duplicates_allowed?(package) + files = package&.package_files || [] + current_maven_files = files.map { |file| extname(file.file_name) } + + if current_maven_files.compact.include?(extname(params[:file_name])) + return ServiceResponse.error(message: 'Duplicate package is not allowed') + end + end + unless package # Maven uploads several files during `mvn deploy` in next order: # - my-company/my-app/1.0-SNAPSHOT/my-app.jar @@ -48,7 +57,15 @@ module Packages package.build_infos.safe_find_or_create_by!(pipeline: params[:build].pipeline) if params[:build].present? - package + ServiceResponse.success(payload: { package: package }) + end + + private + + def extname(filename) + return if filename.blank? + + File.extname(filename) end end end diff --git a/app/services/packages/nuget/search_service.rb b/app/services/packages/nuget/search_service.rb index b95aa30bec1..1eead1e62b3 100644 --- a/app/services/packages/nuget/search_service.rb +++ b/app/services/packages/nuget/search_service.rb @@ -3,6 +3,7 @@ module Packages module Nuget class SearchService < BaseService + include ::Packages::FinderHelper include Gitlab::Utils::StrongMemoize include ActiveRecord::ConnectionAdapters::Quoting @@ -16,8 +17,9 @@ module Packages padding: 0 }.freeze - def initialize(project, search_term, options = {}) - @project = project + def initialize(current_user, project_or_group, search_term, options = {}) + @current_user = current_user + @project_or_group = project_or_group @search_term = search_term @options = DEFAULT_OPTIONS.merge(options) @@ -26,8 +28,8 @@ module Packages end def execute - OpenStruct.new( - total_count: package_names.total_count, + Result.new( + total_count: non_paginated_matching_package_names.count, results: search_packages ) end @@ -39,52 +41,104 @@ module Packages # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24182#technical-notes # and https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource subquery_name = :partition_subquery - arel_table = Arel::Table.new(:partition_subquery) + arel_table = Arel::Table.new(subquery_name) column_names = Packages::Package.column_names.map do |cn| "#{subquery_name}.#{quote_column_name(cn)}" end # rubocop: disable CodeReuse/ActiveRecord - pkgs = Packages::Package.select(column_names.join(',')) - .from(package_names_partition, subquery_name) - .where(arel_table[:row_number].lteq(MAX_VERSIONS_PER_PACKAGE)) + pkgs = Packages::Package + pkgs = pkgs.with(project_ids_cte.to_arel) if use_project_ids_cte? + pkgs = pkgs.select(column_names.join(',')) + .from(package_names_partition, subquery_name) + .where(arel_table[:row_number].lteq(MAX_VERSIONS_PER_PACKAGE)) return pkgs if include_prerelease_versions? # we can't use pkgs.without_version_like since we have a custom from pkgs.where.not(arel_table[:version].matches(PRE_RELEASE_VERSION_MATCHING_TERM)) + # rubocop: enable CodeReuse/ActiveRecord end def package_names_partition + # rubocop: disable CodeReuse/ActiveRecord table_name = quote_table_name(Packages::Package.table_name) name_column = "#{table_name}.#{quote_column_name('name')}" created_at_column = "#{table_name}.#{quote_column_name('created_at')}" select_sql = "ROW_NUMBER() OVER (PARTITION BY #{name_column} ORDER BY #{created_at_column} DESC) AS row_number, #{table_name}.*" - @project.packages - .select(select_sql) - .nuget - .has_version - .without_nuget_temporary_name - .with_name(package_names) + nuget_packages.select(select_sql) + .with_name(paginated_matching_package_names) + .where(project_id: project_ids) + # rubocop: enable CodeReuse/ActiveRecord end - def package_names - strong_memoize(:package_names) do - pkgs = @project.packages - .nuget - .has_version - .without_nuget_temporary_name - .order_name - .select_distinct_name + def paginated_matching_package_names + pkgs = base_matching_package_names + pkgs.page(0) # we're using a padding + .per(per_page) + .padding(padding) + end + + def non_paginated_matching_package_names + # rubocop: disable CodeReuse/ActiveRecord + pkgs = base_matching_package_names + pkgs = pkgs.with(project_ids_cte.to_arel) if use_project_ids_cte? + pkgs + # rubocop: enable CodeReuse/ActiveRecord + end + + def base_matching_package_names + strong_memoize(:base_matching_package_names) do + # rubocop: disable CodeReuse/ActiveRecord + pkgs = nuget_packages.order_name + .select_distinct_name + .where(project_id: project_ids) pkgs = pkgs.without_version_like(PRE_RELEASE_VERSION_MATCHING_TERM) unless include_prerelease_versions? pkgs = pkgs.search_by_name(@search_term) if @search_term.present? - pkgs.page(0) # we're using a padding - .per(per_page) - .padding(padding) + pkgs + # rubocop: enable CodeReuse/ActiveRecord end end + def nuget_packages + Packages::Package.nuget + .has_version + .without_nuget_temporary_name + end + + def project_ids_cte + return unless use_project_ids_cte? + + strong_memoize(:project_ids_cte) do + query = projects_visible_to_user(@current_user, within_group: @project_or_group) + Gitlab::SQL::CTE.new(:project_ids, query.select(:id)) + end + end + + def project_ids + return @project_or_group.id if project? + + if use_project_ids_cte? + # rubocop: disable CodeReuse/ActiveRecord + Project.select(:id) + .from(project_ids_cte.table) + # rubocop: enable CodeReuse/ActiveRecord + end + end + + def use_project_ids_cte? + group? + end + + def project? + @project_or_group.is_a?(::Project) + end + + def group? + @project_or_group.is_a?(::Group) + end + def include_prerelease_versions? @options[:include_prerelease_versions] end @@ -96,6 +150,12 @@ module Packages def per_page [@options[:per_page], MAX_PER_PAGE].min end + + class Result + include ActiveModel::Model + + attr_accessor :results, :total_count + end end end end diff --git a/app/services/pages/migrate_legacy_storage_to_deployment_service.rb b/app/services/pages/migrate_legacy_storage_to_deployment_service.rb new file mode 100644 index 00000000000..dac994b2ccc --- /dev/null +++ b/app/services/pages/migrate_legacy_storage_to_deployment_service.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Pages + class MigrateLegacyStorageToDeploymentService + ExclusiveLeaseTakenError = Class.new(StandardError) + + include BaseServiceUtility + include ::Pages::LegacyStorageLease + + attr_reader :project + + def initialize(project) + @project = project + end + + def execute + result = try_obtain_lease do + execute_unsafe + end + + raise ExclusiveLeaseTakenError, "Can't migrate pages for project #{project.id}: exclusive lease taken" if result.nil? + + result + end + + private + + def execute_unsafe + zip_result = ::Pages::ZipDirectoryService.new(project.pages_path).execute + + if zip_result[:status] == :error + if !project.pages_metadatum&.reload&.pages_deployment && + Feature.enabled?(:pages_migration_mark_as_not_deployed, project) + project.mark_pages_as_not_deployed + end + + return error("Can't create zip archive: #{zip_result[:message]}") + end + + archive_path = zip_result[:archive_path] + + deployment = nil + File.open(archive_path) do |file| + deployment = project.pages_deployments.create!( + file: file, + file_count: zip_result[:entries_count], + file_sha256: Digest::SHA256.file(archive_path).hexdigest + ) + end + + project.set_first_pages_deployment!(deployment) + + success + ensure + FileUtils.rm_f(archive_path) if archive_path + end + end +end diff --git a/app/services/pages/zip_directory_service.rb b/app/services/pages/zip_directory_service.rb index a27ad5fda46..ba7a8571e88 100644 --- a/app/services/pages/zip_directory_service.rb +++ b/app/services/pages/zip_directory_service.rb @@ -2,37 +2,43 @@ module Pages class ZipDirectoryService - InvalidArchiveError = Class.new(RuntimeError) - InvalidEntryError = Class.new(RuntimeError) + include BaseServiceUtility + include Gitlab::Utils::StrongMemoize + + # used only to track exceptions in Sentry + InvalidEntryError = Class.new(StandardError) PUBLIC_DIR = 'public' def initialize(input_dir) - @input_dir = File.realpath(input_dir) - @output_file = File.join(@input_dir, "@migrated.zip") # '@' to avoid any name collision with groups or projects + @input_dir = input_dir end def execute - FileUtils.rm_f(@output_file) + return error("Can not find valid public dir in #{@input_dir}") unless valid_path?(public_dir) + + output_file = File.join(real_dir, "@migrated.zip") # '@' to avoid any name collision with groups or projects + + FileUtils.rm_f(output_file) - count = 0 - ::Zip::File.open(@output_file, ::Zip::File::CREATE) do |zipfile| + entries_count = 0 + ::Zip::File.open(output_file, ::Zip::File::CREATE) do |zipfile| write_entry(zipfile, PUBLIC_DIR) - count = zipfile.entries.count + entries_count = zipfile.entries.count end - [@output_file, count] + success(archive_path: output_file, entries_count: entries_count) + rescue => e + FileUtils.rm_f(output_file) if output_file + raise e end private def write_entry(zipfile, zipfile_path) - disk_file_path = File.join(@input_dir, zipfile_path) + disk_file_path = File.join(real_dir, zipfile_path) unless valid_path?(disk_file_path) - # archive without public directory is completelly unusable - raise InvalidArchiveError if zipfile_path == PUBLIC_DIR - # archive with invalid entry will just have this entry missing raise InvalidEntryError end @@ -71,13 +77,24 @@ module Pages def valid_path?(disk_file_path) realpath = File.realpath(disk_file_path) - realpath == File.join(@input_dir, PUBLIC_DIR) || - realpath.start_with?(File.join(@input_dir, PUBLIC_DIR + "/")) + realpath == public_dir || realpath.start_with?(public_dir + "/") # happens if target of symlink isn't there rescue => e - Gitlab::ErrorTracking.track_exception(e, input_dir: @input_dir, disk_file_path: disk_file_path) + Gitlab::ErrorTracking.track_exception(e, input_dir: real_dir, disk_file_path: disk_file_path) false end + + def real_dir + strong_memoize(:real_dir) do + File.realpath(@input_dir) rescue nil + end + end + + def public_dir + strong_memoize(:public_dir) do + File.join(real_dir, PUBLIC_DIR) rescue nil + end + end end end diff --git a/app/services/post_receive_service.rb b/app/services/post_receive_service.rb index bd9588844ad..84d9db5435b 100644 --- a/app/services/post_receive_service.rb +++ b/app/services/post_receive_service.rb @@ -94,7 +94,7 @@ class PostReceiveService end def record_onboarding_progress - NamespaceOnboardingAction.create_action(project.namespace, :git_write) + OnboardingProgressService.new(project.namespace).execute(action: :git_write) end end diff --git a/app/services/projects/after_import_service.rb b/app/services/projects/after_import_service.rb index b37ae56ba0f..bb0d084d191 100644 --- a/app/services/projects/after_import_service.rb +++ b/app/services/projects/after_import_service.rb @@ -9,7 +9,7 @@ module Projects end def execute - service = Projects::HousekeepingService.new(@project) + service = Repositories::HousekeepingService.new(@project) service.execute do import_failure_service.with_retry(action: 'delete_all_refs') do @@ -21,7 +21,7 @@ module Projects # import actually changed, so we increment the counter to avoid # causing GC to run every time. service.increment! - rescue Projects::HousekeepingService::LeaseTaken => e + rescue Repositories::HousekeepingService::LeaseTaken => e Gitlab::Import::Logger.info( message: 'Project housekeeping failed', project_full_path: @project.full_path, diff --git a/app/services/projects/container_repository/cleanup_tags_service.rb b/app/services/projects/container_repository/cleanup_tags_service.rb index b8047a1ad71..af0107436c8 100644 --- a/app/services/projects/container_repository/cleanup_tags_service.rb +++ b/app/services/projects/container_repository/cleanup_tags_service.rb @@ -8,12 +8,26 @@ module Projects return error('invalid regex') unless valid_regex? tags = container_repository.tags + original_size = tags.size + tags = without_latest(tags) tags = filter_by_name(tags) + + before_truncate_size = tags.size + tags = truncate(tags) + after_truncate_size = tags.size + tags = filter_keep_n(tags) tags = filter_by_older_than(tags) - delete_tags(container_repository, tags) + delete_tags(container_repository, tags).tap do |result| + result[:original_size] = original_size + result[:before_truncate_size] = before_truncate_size + result[:after_truncate_size] = after_truncate_size + result[:before_delete_size] = tags.size + + result[:status] = :error if before_truncate_size != after_truncate_size + end end private @@ -23,12 +37,14 @@ module Projects tag_names = tags.map(&:name) - Projects::ContainerRepository::DeleteTagsService - .new(container_repository.project, - current_user, - tags: tag_names, - container_expiration_policy: params['container_expiration_policy']) - .execute(container_repository) + service = Projects::ContainerRepository::DeleteTagsService.new( + container_repository.project, + current_user, + tags: tag_names, + container_expiration_policy: params['container_expiration_policy'] + ) + + service.execute(container_repository) end def without_latest(tags) @@ -54,7 +70,7 @@ module Projects return tags unless params['keep_n'] tags = order_by_date(tags) - tags.drop(params['keep_n'].to_i) + tags.drop(keep_n) end def filter_by_older_than(tags) @@ -83,6 +99,31 @@ module Projects ::Gitlab::ErrorTracking.log_exception(e, project_id: project.id) false end + + def truncate(tags) + return tags unless throttling_enabled? + return tags if max_list_size == 0 + + # truncate the list to make sure that after the #filter_keep_n + # execution, the resulting list will be max_list_size + truncated_size = max_list_size + keep_n + + return tags if tags.size <= truncated_size + + tags.sample(truncated_size) + end + + def throttling_enabled? + Feature.enabled?(:container_registry_expiration_policies_throttling) + end + + def max_list_size + ::Gitlab::CurrentSettings.current_application_settings.container_registry_cleanup_tags_service_max_list_size.to_i + end + + def keep_n + params['keep_n'].to_i + end end end end diff --git a/app/services/projects/container_repository/gitlab/delete_tags_service.rb b/app/services/projects/container_repository/gitlab/delete_tags_service.rb index e4e22dd9543..589aac5c3ac 100644 --- a/app/services/projects/container_repository/gitlab/delete_tags_service.rb +++ b/app/services/projects/container_repository/gitlab/delete_tags_service.rb @@ -24,9 +24,9 @@ module Projects return success(deleted: []) if @tag_names.empty? delete_tags - rescue TimeoutError => e + rescue TimeoutError, ::Faraday::Error => e ::Gitlab::ErrorTracking.track_exception(e, tags_count: @tag_names&.size, container_repository_id: @container_repository&.id) - error('timeout while deleting tags', nil, pass_back: { deleted: @deleted_tags }) + error('error while deleting tags', nil, pass_back: { deleted: @deleted_tags, exception_class_name: e.class.name }) end private diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 0b4963e356a..050bfdd862d 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -34,11 +34,6 @@ module Projects new_project = CreateService.new(current_user, new_fork_params).execute return new_project unless new_project.persisted? - # Set the forked_from_project relation after saving to avoid having to - # reload the project to reset the association information and cause an - # extra query. - new_project.forked_from_project = @project - builds_access_level = @project.project_feature.builds_access_level new_project.project_feature.update(builds_access_level: builds_access_level) @@ -47,6 +42,7 @@ module Projects def new_fork_params new_params = { + forked_from_project: @project, visibility_level: allowed_visibility_level, description: @project.description, name: target_name, diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb index 9428575591e..b5589d556aa 100644 --- a/app/services/projects/housekeeping_service.rb +++ b/app/services/projects/housekeeping_service.rb @@ -1,107 +1,16 @@ # frozen_string_literal: true -# Projects::HousekeepingService class +# This is a compatibility class to avoid calling a non-existent +# class from sidekiq during deployment. # -# Used for git housekeeping +# We're deploying the rename of this class in 13.9. Nevertheless, +# we cannot remove this class entirely because there can be jobs +# referencing it. # -# Ex. -# Projects::HousekeepingService.new(project).execute +# We can get rid of this class in 13.10 +# https://gitlab.com/gitlab-org/gitlab/-/issues/297580 # module Projects - class HousekeepingService < BaseService - # Timeout set to 24h - LEASE_TIMEOUT = 86400 - PACK_REFS_PERIOD = 6 - - class LeaseTaken < StandardError - def to_s - "Somebody already triggered housekeeping for this project in the past #{LEASE_TIMEOUT / 60} minutes" - end - end - - def initialize(project, task = nil) - @project = project - @task = task - end - - def execute - lease_uuid = try_obtain_lease - raise LeaseTaken unless lease_uuid.present? - - yield if block_given? - - execute_gitlab_shell_gc(lease_uuid) - end - - def needed? - pushes_since_gc > 0 && period_match? && housekeeping_enabled? - end - - def increment! - Gitlab::Metrics.measure(:increment_pushes_since_gc) do - @project.increment_pushes_since_gc - end - end - - private - - def execute_gitlab_shell_gc(lease_uuid) - GitGarbageCollectWorker.perform_async(@project.id, task, lease_key, lease_uuid) - ensure - if pushes_since_gc >= gc_period - Gitlab::Metrics.measure(:reset_pushes_since_gc) do - @project.reset_pushes_since_gc - end - end - end - - def try_obtain_lease - Gitlab::Metrics.measure(:obtain_housekeeping_lease) do - lease = ::Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT) - lease.try_obtain - end - end - - def lease_key - "project_housekeeping:#{@project.id}" - end - - def pushes_since_gc - @project.pushes_since_gc - end - - def task - return @task if @task - - if pushes_since_gc % gc_period == 0 - :gc - elsif pushes_since_gc % full_repack_period == 0 - :full_repack - elsif pushes_since_gc % repack_period == 0 - :incremental_repack - else - :pack_refs - end - end - - def period_match? - [gc_period, full_repack_period, repack_period, PACK_REFS_PERIOD].any? { |period| pushes_since_gc % period == 0 } - end - - def housekeeping_enabled? - Gitlab::CurrentSettings.housekeeping_enabled - end - - def gc_period - Gitlab::CurrentSettings.housekeeping_gc_period - end - - def full_repack_period - Gitlab::CurrentSettings.housekeeping_full_repack_period - end - - def repack_period - Gitlab::CurrentSettings.housekeeping_incremental_repack_period - end + class HousekeepingService < ::Repositories::HousekeepingService end end diff --git a/app/services/projects/schedule_bulk_repository_shard_moves_service.rb b/app/services/projects/schedule_bulk_repository_shard_moves_service.rb index dd49910207f..53de9abdb59 100644 --- a/app/services/projects/schedule_bulk_repository_shard_moves_service.rb +++ b/app/services/projects/schedule_bulk_repository_shard_moves_service.rb @@ -3,33 +3,29 @@ module Projects # Tries to schedule a move for every project with repositories on the source shard class ScheduleBulkRepositoryShardMovesService - include BaseServiceUtility + include ScheduleBulkRepositoryShardMovesMethods + extend ::Gitlab::Utils::Override - def execute(source_storage_name, destination_storage_name = nil) - shard = Shard.find_by_name!(source_storage_name) + private - ProjectRepository.for_shard(shard).each_batch(column: :project_id) do |relation| - Project.id_in(relation.select(:project_id)).each do |project| - project.with_lock do - next if project.repository_storage != source_storage_name - - storage_move = project.repository_storage_moves.build( - source_storage_name: source_storage_name, - destination_storage_name: destination_storage_name - ) + override :repository_klass + def repository_klass + ProjectRepository + end - unless storage_move.schedule - log_info("Project #{project.full_path} (#{project.id}) was skipped: #{storage_move.errors.full_messages.to_sentence}") - end - end - end - end + override :container_klass + def container_klass + Project + end - success + override :container_column + def container_column + :project_id end - def self.enqueue(source_storage_name, destination_storage_name = nil) - ::ProjectScheduleBulkRepositoryShardMovesWorker.perform_async(source_storage_name, destination_storage_name) + override :schedule_bulk_worker_klass + def self.schedule_bulk_worker_klass + ::ProjectScheduleBulkRepositoryShardMovesWorker end end end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 1574c90d2ac..8a5e0706126 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -37,7 +37,7 @@ module Projects private - attr_reader :old_path, :new_path, :new_namespace + attr_reader :old_path, :new_path, :new_namespace, :old_namespace # rubocop: disable CodeReuse/ActiveRecord def transfer(project) @@ -96,7 +96,7 @@ module Projects execute_system_hooks end - move_pages(project) + post_update_hooks(project) rescue Exception # rubocop:disable Lint/RescueException rollback_side_effects raise @@ -104,6 +104,11 @@ module Projects refresh_permissions end + # Overridden in EE + def post_update_hooks(project) + move_pages(project) + end + def transfer_missing_group_resources(group) Labels::TransferService.new(current_user, group, project).execute diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb index 52aea8c51a5..6ba3356d612 100644 --- a/app/services/projects/unlink_fork_service.rb +++ b/app/services/projects/unlink_fork_service.rb @@ -9,6 +9,8 @@ module Projects return unless fork_network + log_info(message: "UnlinkForkService: Unlinking fork network", fork_network_id: fork_network.id) + merge_requests = fork_network .merge_requests .opened @@ -16,6 +18,7 @@ module Projects merge_requests.find_each do |mr| ::MergeRequests::CloseService.new(@project, @current_user).execute(mr) + log_info(message: "UnlinkForkService: Closed merge request", merge_request_id: mr.id) end Project.transaction do @@ -31,6 +34,16 @@ module Projects end end + # rubocop: disable Cop/InBatches + Project.uncached do + @project.forked_to_members.in_batches do |fork_relation| + fork_relation.pluck(:id).each do |fork_id| # rubocop: disable CodeReuse/ActiveRecord + log_info(message: "UnlinkForkService: Unlinked fork of root_project", project_id: @project.id, forked_project_id: fork_id) + end + end + end + # rubocop: enable Cop/InBatches + # When the project getting out of the network is a node with parent # and children, both the parent and the node needs a cache refresh. [forked_from, @project].compact.each do |project| diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index 53872c67f49..25d46ada885 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -37,16 +37,11 @@ module Projects raise InvalidStateError, 'missing pages artifacts' unless build.artifacts? raise InvalidStateError, 'build SHA is outdated for this ref' unless latest? - # Create temporary directory in which we will extract the artifacts - make_secure_tmp_dir(tmp_path) do |archive_path| - extract_archive!(archive_path) + build.artifacts_file.use_file do |artifacts_path| + deploy_to_legacy_storage(artifacts_path) - # Check if we did extract public directory - archive_public_path = File.join(archive_path, PUBLIC_DIR) - raise InvalidStateError, 'pages miss the public folder' unless Dir.exist?(archive_public_path) - raise InvalidStateError, 'build SHA is outdated for this ref' unless latest? + create_pages_deployment(artifacts_path, build) - deploy_page!(archive_public_path) success end rescue InvalidStateError => e @@ -84,15 +79,29 @@ module Projects ) end - def extract_archive!(temp_path) + def deploy_to_legacy_storage(artifacts_path) + # Create temporary directory in which we will extract the artifacts + make_secure_tmp_dir(tmp_path) do |tmp_path| + extract_archive!(artifacts_path, tmp_path) + + # Check if we did extract public directory + archive_public_path = File.join(tmp_path, PUBLIC_DIR) + raise InvalidStateError, 'pages miss the public folder' unless Dir.exist?(archive_public_path) + raise InvalidStateError, 'build SHA is outdated for this ref' unless latest? + + deploy_page!(archive_public_path) + end + end + + def extract_archive!(artifacts_path, temp_path) if artifacts.ends_with?('.zip') - extract_zip_archive!(temp_path) + extract_zip_archive!(artifacts_path, temp_path) else raise InvalidStateError, 'unsupported artifacts format' end end - def extract_zip_archive!(temp_path) + def extract_zip_archive!(artifacts_path, temp_path) raise InvalidStateError, 'missing artifacts metadata' unless build.artifacts_metadata? # Calculate page size after extract @@ -102,11 +111,8 @@ module Projects raise InvalidStateError, "artifacts for pages are too large: #{public_entry.total_size}" end - build.artifacts_file.use_file do |artifacts_path| - SafeZip::Extract.new(artifacts_path) - .extract(directories: [PUBLIC_DIR], to: temp_path) - create_pages_deployment(artifacts_path, build) - end + SafeZip::Extract.new(artifacts_path) + .extract(directories: [PUBLIC_DIR], to: temp_path) rescue SafeZip::Extract::Error => e raise FailedToExtractError, e.message end @@ -150,6 +156,9 @@ module Projects deployment = project.pages_deployments.create!(file: file, file_count: entries_count, file_sha256: sha256) + + raise InvalidStateError, 'build SHA is outdated for this ref' unless latest? + project.update_pages_deployment!(deployment) end diff --git a/app/services/projects/update_repository_storage_service.rb b/app/services/projects/update_repository_storage_service.rb index e0d2398bc66..7c63216af5e 100644 --- a/app/services/projects/update_repository_storage_service.rb +++ b/app/services/projects/update_repository_storage_service.rb @@ -2,59 +2,19 @@ module Projects class UpdateRepositoryStorageService - Error = Class.new(StandardError) - SameFilesystemError = Class.new(Error) + include UpdateRepositoryStorageMethods - attr_reader :repository_storage_move - delegate :project, :source_storage_name, :destination_storage_name, to: :repository_storage_move - - def initialize(repository_storage_move) - @repository_storage_move = repository_storage_move - end - - def execute - repository_storage_move.with_lock do - return ServiceResponse.success unless repository_storage_move.scheduled? # rubocop:disable Cop/AvoidReturnFromBlocks - - repository_storage_move.start! - end - - raise SameFilesystemError if same_filesystem?(source_storage_name, destination_storage_name) - - mirror_repositories - - repository_storage_move.transaction do - repository_storage_move.finish_replication! - - project.leave_pool_repository - project.track_project_repository - end - - remove_old_paths - enqueue_housekeeping - - repository_storage_move.finish_cleanup! - - ServiceResponse.success - - rescue StandardError => e - repository_storage_move.do_fail! - - Gitlab::ErrorTracking.track_exception(e, project_path: project.full_path) - - ServiceResponse.error( - message: s_("UpdateRepositoryStorage|Error moving repository storage for %{project_full_path} - %{message}") % { project_full_path: project.full_path, message: e.message } - ) - end + delegate :project, to: :repository_storage_move private - def same_filesystem?(old_storage, new_storage) - Gitlab::GitalyClient.filesystem_id(old_storage) == Gitlab::GitalyClient.filesystem_id(new_storage) + def track_repository(_destination_storage_name) + project.leave_pool_repository + project.track_project_repository end def mirror_repositories - mirror_repository if project.repository_exists? + mirror_repository(type: Gitlab::GlRepository::PROJECT) if project.repository_exists? if project.wiki.repository_exists? mirror_repository(type: Gitlab::GlRepository::WIKI) @@ -65,41 +25,21 @@ module Projects end end - def mirror_repository(type: Gitlab::GlRepository::PROJECT) - unless wait_for_pushes(type) - raise Error, s_('UpdateRepositoryStorage|Timeout waiting for %{type} repository pushes') % { type: type.name } - end - - repository = type.repository_for(project) - full_path = repository.full_path - raw_repository = repository.raw - checksum = repository.checksum - - # Initialize a git repository on the target path - new_repository = Gitlab::Git::Repository.new( - destination_storage_name, - raw_repository.relative_path, - raw_repository.gl_repository, - full_path - ) - - new_repository.replicate(raw_repository) - new_checksum = new_repository.checksum + # The underlying FetchInternalRemote call uses a `git fetch` to move data + # to the new repository, which leaves it in a less-well-packed state, + # lacking bitmaps and commit graphs. Housekeeping will boost performance + # significantly. + def enqueue_housekeeping + return unless Gitlab::CurrentSettings.housekeeping_enabled? + return unless Feature.enabled?(:repack_after_shard_migration, project) - if checksum != new_checksum - raise Error, s_('UpdateRepositoryStorage|Failed to verify %{type} repository checksum from %{old} to %{new}') % { type: type.name, old: checksum, new: new_checksum } - end + Repositories::HousekeepingService.new(project, :gc).execute + rescue Repositories::HousekeepingService::LeaseTaken + # No action required end def remove_old_paths - if project.repository_exists? - Gitlab::Git::Repository.new( - source_storage_name, - "#{project.disk_path}.git", - nil, - nil - ).remove - end + super if project.wiki.repository_exists? Gitlab::Git::Repository.new( @@ -119,31 +59,5 @@ module Projects ).remove end end - - # The underlying FetchInternalRemote call uses a `git fetch` to move data - # to the new repository, which leaves it in a less-well-packed state, - # lacking bitmaps and commit graphs. Housekeeping will boost performance - # significantly. - def enqueue_housekeeping - return unless Gitlab::CurrentSettings.housekeeping_enabled? - return unless Feature.enabled?(:repack_after_shard_migration, project) - - Projects::HousekeepingService.new(project, :gc).execute - rescue Projects::HousekeepingService::LeaseTaken - # No action required - end - - def wait_for_pushes(type) - reference_counter = project.reference_counter(type: type) - - # Try for 30 seconds, polling every 10 - 3.times do - return true if reference_counter.value == 0 - - sleep 10 - end - - false - end end end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index d44f5e637f1..50a544ed1a5 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -87,11 +87,6 @@ module Projects system_hook_service.execute_hooks_for(project, :update) end - if project.visibility_level_decreased? && project.unlink_forks_upon_visibility_decrease_enabled? - # It's a system-bounded operation, so no extra authorization check is required. - Projects::UnlinkForkService.new(project, current_user).execute - end - update_pages_config if changing_pages_related_config? end diff --git a/app/services/repositories/housekeeping_service.rb b/app/services/repositories/housekeeping_service.rb new file mode 100644 index 00000000000..6a2fa95d25f --- /dev/null +++ b/app/services/repositories/housekeeping_service.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +# Used for git housekeeping +# +# Ex. +# Repositories::HousekeepingService.new(project).execute +# Repositories::HousekeepingService.new(project.wiki).execute +# +module Repositories + class HousekeepingService < BaseService + # Timeout set to 24h + LEASE_TIMEOUT = 86400 + PACK_REFS_PERIOD = 6 + + class LeaseTaken < StandardError + def to_s + "Somebody already triggered housekeeping for this resource in the past #{LEASE_TIMEOUT / 60} minutes" + end + end + + def initialize(resource, task = nil) + @resource = resource + @task = task + end + + def execute + lease_uuid = try_obtain_lease + raise LeaseTaken unless lease_uuid.present? + + yield if block_given? + + execute_gitlab_shell_gc(lease_uuid) + end + + def needed? + pushes_since_gc > 0 && period_match? && housekeeping_enabled? + end + + def increment! + Gitlab::Metrics.measure(:increment_pushes_since_gc) do + @resource.increment_pushes_since_gc + end + end + + private + + def execute_gitlab_shell_gc(lease_uuid) + GitGarbageCollectWorker.perform_async(@resource.id, task, lease_key, lease_uuid) + ensure + if pushes_since_gc >= gc_period + Gitlab::Metrics.measure(:reset_pushes_since_gc) do + @resource.reset_pushes_since_gc + end + end + end + + def try_obtain_lease + Gitlab::Metrics.measure(:obtain_housekeeping_lease) do + lease = ::Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT) + lease.try_obtain + end + end + + def lease_key + "#{@resource.class.name.underscore.pluralize}_housekeeping:#{@resource.id}" + end + + def pushes_since_gc + @resource.pushes_since_gc + end + + def task + return @task if @task + + if pushes_since_gc % gc_period == 0 + :gc + elsif pushes_since_gc % full_repack_period == 0 + :full_repack + elsif pushes_since_gc % repack_period == 0 + :incremental_repack + else + :pack_refs + end + end + + def period_match? + [gc_period, full_repack_period, repack_period, PACK_REFS_PERIOD].any? { |period| pushes_since_gc % period == 0 } + end + + def housekeeping_enabled? + Gitlab::CurrentSettings.housekeeping_enabled + end + + def gc_period + Gitlab::CurrentSettings.housekeeping_gc_period + end + + def full_repack_period + Gitlab::CurrentSettings.housekeeping_full_repack_period + end + + def repack_period + Gitlab::CurrentSettings.housekeeping_incremental_repack_period + end + end +end diff --git a/app/services/resource_events/change_state_service.rb b/app/services/resource_events/change_state_service.rb index cd6d82df46f..c5120ba82e1 100644 --- a/app/services/resource_events/change_state_service.rb +++ b/app/services/resource_events/change_state_service.rb @@ -19,7 +19,7 @@ module ResourceEvents state: ResourceStateEvent.states[state], close_after_error_tracking_resolve: close_after_error_tracking_resolve, close_auto_resolve_prometheus_alert: close_auto_resolve_prometheus_alert, - created_at: Time.zone.now + created_at: resource.system_note_timestamp ) resource.expire_note_etag_cache diff --git a/app/services/serverless/associate_domain_service.rb b/app/services/serverless/associate_domain_service.rb index 673f1f83260..0c6ee58924c 100644 --- a/app/services/serverless/associate_domain_service.rb +++ b/app/services/serverless/associate_domain_service.rb @@ -2,7 +2,7 @@ module Serverless class AssociateDomainService - PLACEHOLDER_HOSTNAME = 'example.com'.freeze + PLACEHOLDER_HOSTNAME = 'example.com' def initialize(knative, pages_domain_id:, creator:) @knative = knative diff --git a/app/services/service_desk_settings/update_service.rb b/app/services/service_desk_settings/update_service.rb index 32d1c5c1c87..5fe74f1f2ff 100644 --- a/app/services/service_desk_settings/update_service.rb +++ b/app/services/service_desk_settings/update_service.rb @@ -5,10 +5,6 @@ module ServiceDeskSettings def execute settings = ServiceDeskSetting.safe_find_or_create_by!(project_id: project.id) - unless ::Feature.enabled?(:service_desk_custom_address, project, default_enabled: true) - params.delete(:project_key) - end - params[:project_key] = nil if params[:project_key].blank? if settings.update(params) diff --git a/app/services/snippets/schedule_bulk_repository_shard_moves_service.rb b/app/services/snippets/schedule_bulk_repository_shard_moves_service.rb new file mode 100644 index 00000000000..f7bdd0a99a5 --- /dev/null +++ b/app/services/snippets/schedule_bulk_repository_shard_moves_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Snippets + # Tries to schedule a move for every snippet with repositories on the source shard + class ScheduleBulkRepositoryShardMovesService + include ScheduleBulkRepositoryShardMovesMethods + extend ::Gitlab::Utils::Override + + private + + override :repository_klass + def repository_klass + SnippetRepository + end + + override :container_klass + def container_klass + Snippet + end + + override :container_column + def container_column + :snippet_id + end + + override :schedule_bulk_worker_klass + def self.schedule_bulk_worker_klass + ::SnippetScheduleBulkRepositoryShardMovesWorker + end + end +end diff --git a/app/services/snippets/update_repository_storage_service.rb b/app/services/snippets/update_repository_storage_service.rb new file mode 100644 index 00000000000..3addae3b3be --- /dev/null +++ b/app/services/snippets/update_repository_storage_service.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Snippets + class UpdateRepositoryStorageService + include UpdateRepositoryStorageMethods + + delegate :snippet, to: :repository_storage_move + + private + + def track_repository(destination_storage_name) + snippet.track_snippet_repository(destination_storage_name) + end + + def mirror_repositories + return unless snippet.repository_exists? + + mirror_repository(type: Gitlab::GlRepository::SNIPPET) + end + end +end diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 0eb099753cb..12d26fe890b 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -216,7 +216,7 @@ class TodoService def create_todos(users, attributes) Array(users).map do |user| - next if pending_todos(user, attributes).exists? + next if pending_todos(user, attributes).exists? && Feature.disabled?(:multiple_todos, user) issue_type = attributes.delete(:issue_type) track_todo_creation(user, issue_type) @@ -278,7 +278,7 @@ class TodoService create_todos(directly_addressed_users, attributes) # Create Todos for mentioned users - mentioned_users = filter_mentioned_users(parent, note || target, author, skip_users) + mentioned_users = filter_mentioned_users(parent, note || target, author, skip_users + directly_addressed_users) attributes = attributes_for_todo(parent, target, author, Todo::MENTIONED, note) create_todos(mentioned_users, attributes) end diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb index aef07b13cae..5a51b42f9f9 100644 --- a/app/services/web_hook_service.rb +++ b/app/services/web_hook_service.rb @@ -54,7 +54,9 @@ class WebHookService http_status: response.code, message: response.to_s } - rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout, Gitlab::HTTP::BlockedUrlError, Gitlab::HTTP::RedirectionTooDeep, Gitlab::Json::LimitedEncoder::LimitExceeded => e + rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, + Net::OpenTimeout, Net::ReadTimeout, Gitlab::HTTP::BlockedUrlError, Gitlab::HTTP::RedirectionTooDeep, + Gitlab::Json::LimitedEncoder::LimitExceeded, URI::InvalidURIError => e execution_duration = Gitlab::Metrics::System.monotonic_time - start_time log_execution( trigger: hook_name, diff --git a/app/uploaders/packages/debian/distribution_release_file_uploader.rb b/app/uploaders/packages/debian/distribution_release_file_uploader.rb new file mode 100644 index 00000000000..9a30aac6396 --- /dev/null +++ b/app/uploaders/packages/debian/distribution_release_file_uploader.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true +class Packages::Debian::DistributionReleaseFileUploader < GitlabUploader + extend Workhorse::UploadPath + include ObjectStorage::Concern + + storage_options Gitlab.config.packages + + after :store, :schedule_background_upload + + alias_method :upload, :model + + def filename + 'Release' + end + + def store_dir + dynamic_segment + end + + private + + def dynamic_segment + raise ObjectNotReadyError, 'Package model not ready' unless model.id + + Gitlab::HashedPath.new("debian_#{model.class.container_type}_distribution", model.id, root_hash: model.container_id) + end +end diff --git a/app/uploaders/packages/package_file_uploader.rb b/app/uploaders/packages/package_file_uploader.rb index 61fbe2b4c49..4b6dbe5b358 100644 --- a/app/uploaders/packages/package_file_uploader.rb +++ b/app/uploaders/packages/package_file_uploader.rb @@ -22,6 +22,8 @@ class Packages::PackageFileUploader < GitlabUploader def dynamic_segment raise ObjectNotReadyError, "Package model not ready" unless model.id - Gitlab::HashedPath.new('packages', model.package.id, 'files', model.id, root_hash: model.package.project_id) + package_segment = model.package.debian? ? 'debian' : model.package.id + + Gitlab::HashedPath.new('packages', package_segment, 'files', model.id, root_hash: model.package.project_id) end end diff --git a/app/validators/feature_flag_strategies_validator.rb b/app/validators/feature_flag_strategies_validator.rb index e542d52c50a..a933a307626 100644 --- a/app/validators/feature_flag_strategies_validator.rb +++ b/app/validators/feature_flag_strategies_validator.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true class FeatureFlagStrategiesValidator < ActiveModel::EachValidator - STRATEGY_DEFAULT = 'default'.freeze - STRATEGY_GRADUALROLLOUTUSERID = 'gradualRolloutUserId'.freeze - STRATEGY_USERWITHID = 'userWithId'.freeze + STRATEGY_DEFAULT = 'default' + STRATEGY_GRADUALROLLOUTUSERID = 'gradualRolloutUserId' + STRATEGY_USERWITHID = 'userWithId' # Order key names alphabetically STRATEGIES = { STRATEGY_DEFAULT => [].freeze, diff --git a/app/validators/json_schemas/debian_fields.json b/app/validators/json_schemas/debian_fields.json new file mode 100644 index 00000000000..b9f6ad2b31d --- /dev/null +++ b/app/validators/json_schemas/debian_fields.json @@ -0,0 +1,9 @@ +{ + "description": "Debian fields", + "type": "object", + "patternProperties": { + ".*": { + "type": "string" + } + } +} diff --git a/app/validators/json_schemas/http_integration_payload_attribute_mapping.json b/app/validators/json_schemas/http_integration_payload_attribute_mapping.json index e457b8a292b..a194daf5e45 100644 --- a/app/validators/json_schemas/http_integration_payload_attribute_mapping.json +++ b/app/validators/json_schemas/http_integration_payload_attribute_mapping.json @@ -6,7 +6,8 @@ "required": ["path", "type"], "properties": { "path": { "type": "array" }, - "type": { "type": "string" } + "type": { "type": "string" }, + "label": { "type": ["string", "null"] } }, "additionalProperties": false } diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml index ae0da214fb7..c3d7fac6df7 100644 --- a/app/views/admin/abuse_reports/_abuse_report.html.haml +++ b/app/views/admin/abuse_reports/_abuse_report.html.haml @@ -12,7 +12,9 @@ = _('(removed)') %td %strong.subheading.d-block.d-sm-none - = _('Reported by %{reporter}') % { reporter: reporter ? link_to(reporter.name, reporter) : _('(removed)') } + = _('Reported by %{reporter}').html_safe % { reporter: reporter ? link_to(reporter.name, reporter) : _('(removed)') } + .light.gl-display-none.gl-display-sm-block + = link_to(reporter.name, reporter) .light.small = time_ago_with_tooltip(abuse_report.created_at) %td diff --git a/app/views/admin/application_settings/_gitpod.html.haml b/app/views/admin/application_settings/_gitpod.html.haml index 1baec07fa25..7f78cce4575 100644 --- a/app/views/admin/application_settings/_gitpod.html.haml +++ b/app/views/admin/application_settings/_gitpod.html.haml @@ -1,4 +1,3 @@ -- return unless Gitlab::Gitpod.feature_available? - expanded = integration_expanded?('gitpod_') %section.settings.no-animate#js-gitpod-settings{ class: ('expanded' if expanded) } diff --git a/app/views/admin/application_settings/_ip_limits.html.haml b/app/views/admin/application_settings/_ip_limits.html.haml index b06070d15d4..11ffe3f56e3 100644 --- a/app/views/admin/application_settings/_ip_limits.html.haml +++ b/app/views/admin/application_settings/_ip_limits.html.haml @@ -49,5 +49,12 @@ .form-group = f.label :throttle_authenticated_web_period_in_seconds, 'Authenticated web rate limit period in seconds', class: 'label-bold' = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control' + %hr + %h5 + = _('Response text') + .form-group + = f.label :rate_limiting_response_text, class: 'label-bold' do + = _('A plain-text response to show to clients that hit the rate limit.') + = f.text_area :rate_limiting_response_text, placeholder: ::Gitlab::Throttle::DEFAULT_RATE_LIMITING_RESPONSE_TEXT, class: 'form-control', rows: 5 = f.submit 'Save changes', class: "gl-button btn btn-success", data: { qa_selector: 'save_changes_button' } diff --git a/app/views/admin/application_settings/_signup.html.haml b/app/views/admin/application_settings/_signup.html.haml index 2f2d42e297e..92477dff3d8 100644 --- a/app/views/admin/application_settings/_signup.html.haml +++ b/app/views/admin/application_settings/_signup.html.haml @@ -4,7 +4,7 @@ %fieldset .form-group .form-check - = f.check_box :signup_enabled, class: 'form-check-input' + = f.check_box :signup_enabled, class: 'form-check-input', data: { qa_selector: 'signup_enabled_checkbox' } = f.label :signup_enabled, class: 'form-check-label' do Sign-up enabled .form-text.text-muted diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml index b54f1d7c829..2b871d3693c 100644 --- a/app/views/admin/application_settings/_spam.html.haml +++ b/app/views/admin/application_settings/_spam.html.haml @@ -30,6 +30,14 @@ .form-group .form-check + = f.check_box :invisible_captcha_enabled, class: 'form-check-input' + = f.label :invisible_captcha_enabled, class: 'form-check-label' do + = _('Enable Invisible Captcha during sign up') + %span.form-text.text-muted + = _('Helps prevent bots from creating accounts.') + + .form-group + .form-check = f.check_box :akismet_enabled, class: 'form-check-input' = f.label :akismet_enabled, class: 'form-check-label' do Enable Akismet diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml index 2ba7dcefd44..fe83d4b807c 100644 --- a/app/views/admin/application_settings/_usage.html.haml +++ b/app/views/admin/application_settings/_usage.html.haml @@ -33,7 +33,7 @@ %pre.usage-data.js-syntax-highlight.code.highlight.mt-2.d-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } } - else = _('The usage ping is disabled, and cannot be configured through this form.') - - deactivating_usage_ping_path = help_page_path('development/product_analytics/usage_ping', anchor: 'disable-usage-ping') + - deactivating_usage_ping_path = help_page_path('development/usage_ping', anchor: 'disable-usage-ping') - deactivating_usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: deactivating_usage_ping_path } = s_('For more information, see the documentation on %{deactivating_usage_ping_link_start}deactivating the usage ping%{deactivating_usage_ping_link_end}.').html_safe % { deactivating_usage_ping_link_start: deactivating_usage_ping_link_start, deactivating_usage_ping_link_end: '</a>'.html_safe } diff --git a/app/views/admin/application_settings/reporting.html.haml b/app/views/admin/application_settings/reporting.html.haml index c60e44b3864..6ea139844d4 100644 --- a/app/views/admin/application_settings/reporting.html.haml +++ b/app/views/admin/application_settings/reporting.html.haml @@ -11,7 +11,7 @@ %p - recaptcha_v2_link_url = 'https://developers.google.com/recaptcha/docs/versions' - recaptcha_v2_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: recaptcha_v2_link_url } - = _('Enable reCAPTCHA or Akismet and set IP limits. For reCAPTCHA, we currently only support %{recaptcha_v2_link_start}v2%{recaptcha_v2_link_end}').html_safe % { recaptcha_v2_link_start: recaptcha_v2_link_start, recaptcha_v2_link_end: '</a>'.html_safe } + = _('Enable reCAPTCHA, Invisible Captcha, Akismet and set IP limits. For reCAPTCHA, we currently only support %{recaptcha_v2_link_start}v2%{recaptcha_v2_link_end}').html_safe % { recaptcha_v2_link_start: recaptcha_v2_link_start, recaptcha_v2_link_end: '</a>'.html_safe } .settings-content = render 'spam' diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml index bbb47e29bb9..9ba72caa88e 100644 --- a/app/views/admin/background_jobs/show.html.haml +++ b/app/views/admin/background_jobs/show.html.haml @@ -4,5 +4,5 @@ %p.light GitLab uses #{link_to "sidekiq", "http://sidekiq.org/"} library for async job processing %hr -.card +.card.gl-rounded-0 %iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: 0" } diff --git a/app/views/admin/dev_ops_report/_report.html.haml b/app/views/admin/dev_ops_report/_report.html.haml index 24c805d273a..5faadd15ef8 100644 --- a/app/views/admin/dev_ops_report/_report.html.haml +++ b/app/views/admin/dev_ops_report/_report.html.haml @@ -4,7 +4,7 @@ = render 'callout' - if !usage_ping_enabled - #js-devops-empty-state{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_usage_ping_link: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/product_analytics/usage_ping') } } + #js-devops-empty-state{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_usage_ping_link: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/usage_ping') } } - elsif @metric.blank? = render 'no_data' - else diff --git a/app/views/admin/dev_ops_report/show.html.haml b/app/views/admin/dev_ops_report/show.html.haml index 75398f3aa21..c16ef7af76d 100644 --- a/app/views/admin/dev_ops_report/show.html.haml +++ b/app/views/admin/dev_ops_report/show.html.haml @@ -3,7 +3,7 @@ .container .gl-mt-3 - - if Gitlab.ee? && Feature.enabled?(:devops_adoption_feature, default_enabled: true) && License.feature_available?(:devops_adoption) + - if show_adoption? = render_if_exists 'admin/dev_ops_report/devops_tabs' - else = render 'report' diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 386df99717b..b949d08718a 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -80,8 +80,9 @@ = storage_counter(project.statistics.storage_size) %span.float-right.light %span.monospace= project.full_path + '.git' - .card-footer - = paginate @projects, param_name: 'projects_page', theme: 'gitlab' + - unless @projects.size < Kaminari.config.default_per_page + .card-footer + = paginate @projects, param_name: 'projects_page', theme: 'gitlab' - shared_projects = @group.shared_projects.sort_by(&:name) - unless shared_projects.empty? @@ -134,5 +135,6 @@ group: @group, show_controls: false, current_user_is_group_owner: current_user_is_group_owner } - .card-footer - = paginate @members, param_name: 'members_page', theme: 'gitlab' + - unless @members.size < Kaminari.config.default_per_page + .card-footer + = paginate @members, param_name: 'members_page', theme: 'gitlab' diff --git a/app/views/admin/hooks/_form.html.haml b/app/views/admin/hooks/_form.html.haml index 5bc5404fada..e6abd8ff85a 100644 --- a/app/views/admin/hooks/_form.html.haml +++ b/app/views/admin/hooks/_form.html.haml @@ -1,52 +1,33 @@ = form_errors(hook) .form-group - = form.label :url, 'URL', class: 'label-bold' + = form.label :url, _('URL'), class: 'label-bold' = form.text_field :url, class: 'form-control' .form-group - = form.label :token, 'Secret Token', class: 'label-bold' + = form.label :token, _('Secret Token'), class: 'label-bold' = form.text_field :token, class: 'form-control' - %p.form-text.text-muted - Use this token to validate received payloads + %p.form-text.text-muted= _('Use this token to validate received payloads') .form-group - = form.label :url, 'Trigger', class: 'label-bold' - %ul.list-unstyled - %li - .form-text.text-muted - System hook will be triggered on set of events like creating project - or adding ssh key. But you can also enable extra triggers like Push events. - - .gl-mt-3 - = form.check_box :repository_update_events, class: 'float-left' - .gl-ml-6 - = form.label :repository_update_events, class: 'list-label' do - %strong Repository update events - %p.light - This URL will be triggered when repository is updated - %li - = form.check_box :push_events, class: 'float-left' - .gl-ml-6 - = form.label :push_events, class: 'list-label' do - %strong Push events - %p.light - This URL will be triggered for each branch updated to the repository - %li - = form.check_box :tag_push_events, class: 'float-left' - .gl-ml-6 - = form.label :tag_push_events, class: 'list-label' do - %strong Tag push events - %p.light - This URL will be triggered when a new tag is pushed to the repository - %li - = form.check_box :merge_requests_events, class: 'float-left' - .gl-ml-6 - = form.label :merge_requests_events, class: 'list-label' do - %strong Merge request events - %p.light - This URL will be triggered when a merge request is created/updated/merged + = form.label :url, _('Trigger'), class: 'label-bold' + .form-text.text-secondary.gl-mb-5= _('System hook will be triggered on set of events like creating project or adding ssh key. But you can also enable extra triggers like Push events.') + %fieldset.form-group.form-check + = form.check_box :repository_update_events, class: 'form-check-input' + = form.label :repository_update_events, _('Repository update events'), class: 'label-bold form-check-label' + .text-secondary= _('This URL will be triggered when repository is updated') + %fieldset.form-group.form-check + = form.check_box :push_events, class: 'form-check-input' + = form.label :push_events, _('Push events'), class: 'label-bold form-check-label' + .text-secondary= _('This URL will be triggered for each branch updated to the repository') + %fieldset.form-group.form-check + = form.check_box :tag_push_events, class: 'form-check-input' + = form.label :tag_push_events, _('Tag push events'), class: 'label-bold form-check-label' + .text-secondary= _('This URL will be triggered when a new tag is pushed to the repository') + %fieldset.form-group.form-check + = form.check_box :merge_requests_events, class: 'form-check-input' + = form.label :merge_requests_events, _('Merge request events'), class: 'label-bold form-check-label' + .text-secondary= _('This URL will be triggered when a merge request is created/updated/merged') .form-group - = form.label :enable_ssl_verification, 'SSL verification', class: 'label-bold checkbox' + = form.label :enable_ssl_verification, _('SSL verification'), class: 'label-bold checkbox' .form-check = form.check_box :enable_ssl_verification, class: 'form-check-input' - = form.label :enable_ssl_verification, class: 'form-check-label' do - %strong Enable SSL verification + = form.label :enable_ssl_verification, _('Enable SSL verification'), class: 'label-bold form-check-label' diff --git a/app/views/admin/jobs/index.html.haml b/app/views/admin/jobs/index.html.haml index 8eaf84c8df9..ce377eeea54 100644 --- a/app/views/admin/jobs/index.html.haml +++ b/app/views/admin/jobs/index.html.haml @@ -1,3 +1,5 @@ +- add_page_specific_style 'page_bundles/ci_status' + - breadcrumb_title _("Jobs") - page_title _("Jobs") diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 3d3b8c28a17..9f19d3f5d4e 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -5,34 +5,34 @@ .col-sm-6 .bs-callout %p - = (_"A 'Runner' is a process which runs a job. You can set up as many Runners as you need.") + = (_"Runners are processes that pick up and execute CI/CD jobs for GitLab.") %br - = _('Runners can be placed on separate users, servers, even on your local machine.') + = _('You can register runners as separate users, on separate servers, and on your local machine. Register as many runners as you want.') %br %div - %span= _('Each Runner can be in one of the following states and/or belong to one of the following types:') + %span= _('Runners can be:') %ul %li - %span.badge.badge-success shared + %span.badge.badge-pill.gl-badge.sm.badge-success shared \- - = _('Runner runs jobs from all unassigned projects') + = _('Runs jobs from all unassigned projects.') %li - %span.badge.badge-success group + %span.badge.badge-pill.gl-badge.sm.badge-success group \- - = _('Runner runs jobs from all unassigned projects in its group') + = _('Runs jobs from all unassigned projects in its group.') %li - %span.badge.badge-info specific + %span.badge.badge-pill.gl-badge.sm.badge-info specific \- - = _('Runner runs jobs from assigned projects') + = _('Runs jobs from assigned projects.') %li - %span.badge.badge-warning locked + %span.badge.badge-pill.gl-badge.sm.badge-warning locked \- - = _('Runner cannot be assigned to other projects') + = _('Cannot be assigned to other projects.') %li - %span.badge.badge-danger paused + %span.badge.badge-pill.gl-badge.sm.badge-danger paused \- - = _('Runner will not receive any new jobs') + = _('Not available to run jobs.') .col-sm-6 .bs-callout diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 06925964dc5..aca50de3852 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -1,3 +1,5 @@ +- add_page_specific_style 'page_bundles/ci_status' + = content_for :title do %h3.project-title Runner ##{@runner.id} @@ -15,17 +17,17 @@ - if @runner.instance_type? .bs-callout.bs-callout-success - %h4 This Runner will process jobs from ALL UNASSIGNED projects + %h4= _('This runner processes jobs for all unassigned projects.') %p - If you want Runners to build only specific projects, enable them in the table below. - Keep in mind that this is a one way transition. + = _('If you want a runner to build only specific projects, restrict the project in the table below. After you restrict a runner to a project, you cannot change it back to a shared runner.') - elsif @runner.group_type? .bs-callout.bs-callout-success - %h4 This runner will process jobs from all projects in its group and subgroups + %h4= _('This runner processes jobs for all projects in its group and subgroups.') - else .bs-callout.bs-callout-info - %h4 This Runner will process jobs only from ASSIGNED projects - %p You can't make this a shared Runner. + %h4= _('This runner processes jobs for assigned projects only.') + %p + = _('You cannot make this a shared runner.') %hr .gl-mb-6 @@ -33,12 +35,12 @@ .row .col-md-6 - %h4 Restrict projects for this Runner + %h4= _('Restrict projects for this runner') - if @runner.projects.any? %table.table.assigned-projects %thead %tr - %th Assigned projects + %th= _('Assigned projects') - @runner.runner_projects.each do |runner_project| - project = runner_project.project - if project @@ -55,7 +57,7 @@ %table.table.unassigned-projects %thead %tr - %th Project + %th= _('Project') %th %tr @@ -78,15 +80,15 @@ = paginate_without_count @projects .col-md-6 - %h4 Recent jobs served by this Runner + %h4= _('Recent jobs served by this runner') %table.table.ci-table.runner-builds %thead %tr - %th Job - %th Status - %th Project - %th Commit - %th Finished at + %th= _('Job') + %th= _('Status') + %th= _('Project') + %th= _('Commit') + %th= _('Finished at') - @builds.each do |build| - project = build.project diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml index ca6efe9b095..7a34972dfbf 100644 --- a/app/views/admin/system_info/show.html.haml +++ b/app/views/admin/system_info/show.html.haml @@ -3,29 +3,41 @@ .gl-mt-3 .row .col-sm - .bg-light.light-well - %h4= _('CPU') + .bg-light.info-well.p-3 + %h4.page-title.d-flex + .gl-display-flex.gl-align-items-center.gl-justify-content-center + = sprite_icon('pod', size: 18, css_class: 'pod-icon gl-mr-3') + = _('CPU') .data - if @cpus %h2= _('%{cores} cores') % { cores: @cpus.length } - else = sprite_icon('warning-solid', css_class: 'text-warning') = _('Unable to collect CPU info') - .bg-light.light-well.gl-mt-3 - %h4= _('Memory Usage') + .bg-light.info-well.p-3.gl-mt-3 + %h4.page-title.d-flex + .gl-display-flex.gl-align-items-center.gl-justify-content-center + = sprite_icon('status-health', size: 18, css_class: 'pod-icon gl-mr-3') + = _('Memory Usage') .data - if @memory %h2 #{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)} - else = sprite_icon('warning-solid', css_class: 'text-warning') = _('Unable to collect memory info') - .bg-light.light-well.gl-mt-3 - %h4= _('Uptime') + .bg-light.info-well.p-3.gl-mt-3 + %h4.page-title.d-flex + .gl-display-flex.gl-align-items-center.gl-justify-content-center + = sprite_icon('clock', size: 18, css_class: 'pod-icon gl-mr-3') + = _('Uptime') .data %h2= distance_of_time_in_words_to_now(Rails.application.config.booted_at) .col-sm - .bg-light.light-well - %h4= _('Disk Usage') + .bg-light.info-well.p-3 + %h4.page-title.d-flex + .gl-display-flex.gl-align-items-center.gl-justify-content-center + = sprite_icon('disk', size: 18, css_class: 'pod-icon gl-mr-3') + = _('Disk Usage') .data %ul - @disks.each do |disk| diff --git a/app/views/admin/users/_admin_notes.html.haml b/app/views/admin/users/_admin_notes.html.haml index 5d91ba1a1ca..4da70a504f7 100644 --- a/app/views/admin/users/_admin_notes.html.haml +++ b/app/views/admin/users/_admin_notes.html.haml @@ -1,7 +1,7 @@ %fieldset %legend= _('Admin notes') .form-group.row - .col-sm-2.col-form-label.text-right + .col-sm-2.col-form-label = f.label :note, s_('AdminNote|Note') .col-sm-10 = f.text_area :note, class: 'form-control' diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index b86abb893a9..cef16b1881e 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -44,6 +44,7 @@ %small.badge.badge-pill= limited_counter_with_delimiter(User.without_projects) .nav-controls = render_if_exists 'admin/users/admin_email_users' + = render_if_exists 'admin/users/admin_export_user_permissions' = link_to s_('AdminUsers|New user'), new_admin_user_path, class: 'btn gl-button btn-success btn-search float-right' .filtered-search-block.row-content-block.border-top-0 @@ -51,7 +52,7 @@ - if params[:filter].present? = hidden_field_tag "filter", h(params[:filter]) .search-holder - .search-field-holder + .search-field-holder.gl-mb-4 = search_field_tag :search_query, params[:search_query], placeholder: s_('AdminUsers|Search by name, email or username'), class: 'form-control search-text-input js-search-input', spellcheck: false, data: { qa_selector: 'user_search_field' } - if @sort.present? = hidden_field_tag :sort, @sort diff --git a/app/views/ci/runner/_how_to_setup_runner.html.haml b/app/views/ci/runner/_how_to_setup_runner.html.haml index 4ea3b0f0fb9..fc3d5360f9b 100644 --- a/app/views/ci/runner/_how_to_setup_runner.html.haml +++ b/app/views/ci/runner/_how_to_setup_runner.html.haml @@ -1,21 +1,23 @@ -- link = link_to _("Install GitLab Runner"), 'https://docs.gitlab.com/runner/install/', target: '_blank' +- link = link_to _("Install GitLab Runner and ensure it's running."), 'https://docs.gitlab.com/runner/install/', target: '_blank' .gl-mb-3 - %h4= _("Set up a %{type} Runner manually") % { type: type } + %h5= _("Set up a %{type} runner manually") % { type: type } %ol %li = link.html_safe %li - = _("Specify the following URL during the Runner setup:") + = _("Register the runner with this URL:") + %br %code#coordinator_address= root_url(only_path: false) = clipboard_button(target: '#coordinator_address', title: _("Copy URL"), class: "btn-transparent btn-clipboard") - %li - = _("Use the following registration token during setup:") + %br + %br + = _("And this registration token:") + %br %code#registration_token= registration_token = clipboard_button(target: '#registration_token', title: _("Copy token"), class: "btn-transparent btn-clipboard") - .gl-mt-3.gl-mb-3 - = button_to _("Reset runners registration token"), reset_token_url, - method: :put, class: 'gl-button btn btn-default', - data: { confirm: _("Are you sure you want to reset registration token?") } - %li - = _("Start the Runner!") + +.gl-mt-3.gl-mb-3 += button_to _("Reset registration token"), reset_token_url, +method: :put, class: 'gl-button btn btn-default', +data: { confirm: _("Are you sure you want to reset the registration token?") } diff --git a/app/views/ci/runner/_how_to_setup_runner_automatically.html.haml b/app/views/ci/runner/_how_to_setup_runner_automatically.html.haml index 343abf6099e..7140c0f4e7c 100644 --- a/app/views/ci/runner/_how_to_setup_runner_automatically.html.haml +++ b/app/views/ci/runner/_how_to_setup_runner_automatically.html.haml @@ -1,22 +1,21 @@ -.gl-mb-3 - %h4= _('Set up a %{type} Runner automatically') % { type: type } +%h5= _('Set up a %{type} runner automatically') % { type: type } %p - - link_to_help_page = link_to(_('Learn more about Kubernetes'), + - link_to_help_page = link_to(_('Learn more.'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') - = _('You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page } + = _('Register a runner on a Kubernetes cluster. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page } %ol %li - = _('Click the button below to begin the install process by navigating to the Kubernetes page') + = _('Click the button below.') %li - = _('Select an existing Kubernetes cluster or create a new one') + = _('Select an existing Kubernetes cluster or create a new one.') %li - = _('From the Kubernetes cluster details view, install Runner from the applications list') + = _('From the Kubernetes cluster details view, applications list, install GitLab Runner.') -= link_to _('Install Runner on Kubernetes'), += link_to _('Install GitLab Runner on Kubernetes'), clusters_path, class: 'gl-button btn btn-info' diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml index 660fd1a48a7..3f6d60c2620 100644 --- a/app/views/ci/variables/_index.html.haml +++ b/app/views/ci/variables/_index.html.haml @@ -21,7 +21,7 @@ } } - if !@group && @project.group - .settings-header.border-top.prepend-top-20 + .settings-header.border-top.gl-mt-6 = render 'ci/group_variables/header' .settings-content.pr-0 = render 'ci/group_variables/index' diff --git a/app/views/clusters/clusters/gcp/_form.html.haml b/app/views/clusters/clusters/gcp/_form.html.haml index 573b96caae5..50e3d29f974 100644 --- a/app/views/clusters/clusters/gcp/_form.html.haml +++ b/app/views/clusters/clusters/gcp/_form.html.haml @@ -1,4 +1,3 @@ -= javascript_include_tag 'https://apis.google.com/js/api.js' - external_link_icon = sprite_icon('external-link') - zones_link_url = 'https://cloud.google.com/compute/docs/regions-zones/regions-zones' - machine_type_link_url = 'https://cloud.google.com/compute/docs/machine-types' diff --git a/app/views/clusters/clusters/new.html.haml b/app/views/clusters/clusters/new.html.haml index ff33fb46db8..74e336723ba 100644 --- a/app/views/clusters/clusters/new.html.haml +++ b/app/views/clusters/clusters/new.html.haml @@ -2,7 +2,6 @@ - page_title _('Kubernetes Cluster') - active_tab = local_assigns.fetch(:active_tab, 'create') - provider = params[:provider] -= javascript_include_tag 'https://apis.google.com/js/api.js' = render_gcp_signup_offer diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index c34e457dbd9..bf61d0bd1f0 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -65,7 +65,7 @@ data: { data: todo_actions_options, default_label: 'Action' } }) .filter-item.sort-filter .dropdown - %button.dropdown-menu-toggle.dropdown-menu-toggle-sort{ type: 'button', 'data-toggle' => 'dropdown' } + %button.dropdown-menu-toggle.dropdown-menu-toggle-sort{ type: 'button', class: 'gl-xs-w-full!', 'data-toggle' => 'dropdown' } %span.light - if @sort.present? = sort_options_hash[@sort] diff --git a/app/views/devise/shared/_footer.html.haml b/app/views/devise/shared/_footer.html.haml new file mode 100644 index 00000000000..ca1adb48543 --- /dev/null +++ b/app/views/devise/shared/_footer.html.haml @@ -0,0 +1,8 @@ +%hr.footer-fixed +.container.footer-container + .footer-links + - unless public_visibility_restricted? + = link_to _("Explore"), explore_root_path + = link_to _("Help"), help_path + = link_to _("About GitLab"), "https://about.gitlab.com/" += footer_message diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 3c4cbbbc3bd..acbf3b398b0 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -10,7 +10,7 @@ = form_for(resource, as: "new_#{resource_name}", url: url, html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive' }) do |f| .devise-errors = render 'devise/shared/error_messages', resource: resource - - if Feature.enabled?(:invisible_captcha) + - if Gitlab::CurrentSettings.invisible_captcha_enabled = invisible_captcha .name.form-row .col.form-group diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml index b5bfbc7bd1c..d89c4bf0161 100644 --- a/app/views/doorkeeper/authorizations/new.html.haml +++ b/app/views/doorkeeper/authorizations/new.html.haml @@ -38,6 +38,8 @@ = hidden_field_tag :response_type, @pre_auth.response_type = hidden_field_tag :scope, @pre_auth.scope = hidden_field_tag :nonce, @pre_auth.nonce + = hidden_field_tag :code_challenge, @pre_auth.code_challenge + = hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method = submit_tag _("Deny"), class: "gl-button btn btn-danger" = form_tag oauth_authorization_path, method: :post, class: 'inline' do = hidden_field_tag :client_id, @pre_auth.client.uid @@ -46,4 +48,6 @@ = hidden_field_tag :response_type, @pre_auth.response_type = hidden_field_tag :scope, @pre_auth.scope = hidden_field_tag :nonce, @pre_auth.nonce + = hidden_field_tag :code_challenge, @pre_auth.code_challenge + = hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method = submit_tag _("Authorize"), class: "gl-button btn btn-success gl-ml-3", data: { qa_selector: 'authorization_button' } diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 4c1ee5fd3b7..d6662e1fc31 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -9,7 +9,7 @@ - many_refs = event.ref_count.to_i > 1 %span.event-type.d-inline-block.gl-mr-2.pushed= many_refs ? "#{event.action_name} #{event.ref_count} #{event.ref_type.pluralize}" : "#{event.action_name} #{event.ref_type}" - unless many_refs - %span.gl-mr-2 + %span.gl-mr-2.text-truncate - commits_link = project_commits_path(project, event.ref_name) - should_link = event.tag? ? project.repository.tag_exists?(event.ref_name) : project.repository.branch_exists?(event.ref_name) = link_to_if should_link, event.ref_name, commits_link, class: 'ref-name' diff --git a/app/views/explore/projects/_projects.html.haml b/app/views/explore/projects/_projects.html.haml index b2154f71082..a55dfb110f0 100644 --- a/app/views/explore/projects/_projects.html.haml +++ b/app/views/explore/projects/_projects.html.haml @@ -2,4 +2,4 @@ .nothing-here-block %h5= _('Enter at least three characters to search') - else - = render 'shared/projects/list', projects: projects, user: current_user, explore_page: true, pipeline_status: Feature.enabled?(:dashboard_pipeline_status, default_enabled: true) + = render 'shared/projects/list', projects: projects, user: current_user, explore_page: true, pipeline_status: Feature.enabled?(:explore_pipeline_status, type: :ops) diff --git a/app/views/groups/_invite_members_modal.html.haml b/app/views/groups/_invite_members_modal.html.haml index 3aae81cef8d..bd53f73230e 100644 --- a/app/views/groups/_invite_members_modal.html.haml +++ b/app/views/groups/_invite_members_modal.html.haml @@ -1,7 +1,7 @@ - if invite_members_allowed?(group) .js-invite-members-modal{ data: { id: group.id, name: group.name, - is_project: false, + is_project: 'false', access_levels: GroupMember.access_level_roles.to_json, default_access_level: Gitlab::Access::GUEST, help_link: help_page_url('user/permissions') } } diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 33cd90ce5d3..229e04a371a 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -35,8 +35,8 @@ %button.btn.gl-button.js-settings-toggle{ type: 'button' } = expanded ? _('Collapse') : _('Expand') %p - = s_('GroupSettings|Customize your group badges.') - = link_to s_('GroupSettings|Learn more about badges.'), help_page_path('user/project/badges') + = s_('GroupSettings|Customize this group\'s badges.') + = link_to s_('GroupSettings|What are badges?'), help_page_path('user/project/badges') .settings-content = render 'shared/badges/badge_settings' diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index a1527a74898..ab3998be009 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -3,136 +3,80 @@ - show_invited_members = can_manage_members && @invited_members.exists? - show_access_requests = can_manage_members && @requesters.exists? - invited_active = params[:search_invited].present? || params[:invited_members_page].present? -- vue_members_list_enabled = Feature.enabled?(:vue_group_members_list, @group, default_enabled: true) -- filtered_search_enabled = Feature.enabled?(:group_members_filtered_search, @group, default_enabled: true) -- current_user_is_group_owner = @group && @group.has_owner?(current_user) - -- form_item_label_css_class = 'label-bold gl-mr-2 gl-mb-0 gl-py-2 align-self-md-center' .js-remove-member-modal -.project-members-page.gl-mt-3 - %h4 - = _('Group members') - %hr - - if can_manage_members - %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' } - %li.nav-tab{ role: 'presentation' } - %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' }= _('Invite member') - %li.nav-tab{ role: 'presentation' } - %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab', qa_selector: 'invite_group_tab' }, role: 'tab' }= _('Invite group') - .tab-content.gitlab-tab-content - .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' } - = render_invite_member_for_group(@group, @group_member.access_level) - .tab-pane{ id: 'invite-group-pane', role: 'tabpanel' } - = render 'shared/members/invite_group', submit_url: group_group_links_path(@group), access_levels: GroupMember.access_level_roles, default_access_level: @group_member.access_level, group_link_field: 'shared_with_group_id', group_access_field: 'shared_group_access' +.row.gl-mt-3 + .col-lg-12 + .gl-display-flex.gl-flex-wrap + - if can_manage_members + .gl-w-half.gl-xs-w-full + %h4 + = _('Group members') + %p + = html_escape(_('You can invite a new member to %{strong_start}%{group_name}%{strong_end}.')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe } + - if invite_members_allowed?(@group) + .gl-w-half.gl-xs-w-full + .gl-display-flex.gl-flex-wrap.gl-lg-justify-content-end.gl-mx-n2.gl-mb-3 + .js-invite-members-trigger.gl-px-2.gl-sm-w-auto.gl-w-full.gl-mb-4{ data: { classes: 'btn btn-success gl-button gl-mt-3 gl-sm-w-auto gl-w-full', display_text: _('Invite members') } } + = render_if_exists 'groups/invite_members_modal', group: @group + - if can_manage_members && !invite_members_allowed?(@group) + %hr.gl-mt-4 + %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' } + %li.nav-tab{ role: 'presentation' } + %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' }= _('Invite member') + %li.nav-tab{ role: 'presentation' } + %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab', qa_selector: 'invite_group_tab' }, role: 'tab' }= _('Invite group') + .tab-content.gitlab-tab-content + .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' } + = render_invite_member_for_group(@group, @group_member.access_level) + .tab-pane{ id: 'invite-group-pane', role: 'tabpanel' } + = render 'shared/members/invite_group', submit_url: group_group_links_path(@group), access_levels: GroupMember.access_level_roles, default_access_level: @group_member.access_level, group_link_field: 'shared_with_group_id', group_access_field: 'shared_group_access' - = render_if_exists 'groups/group_members/ldap_sync' + = render_if_exists 'groups/group_members/ldap_sync' - %ul.nav-links.mobile-separator.nav.nav-tabs - %li.nav-item - = link_to '#tab-members', class: ['nav-link', ('active' unless invited_active)], data: { toggle: 'tab' } do - %span - = _('Members') - %span.badge.badge-pill= @members.total_count - - if @group.shared_with_group_links.any? - %li.nav-item - = link_to '#tab-groups', class: ['nav-link'] , data: { toggle: 'tab', qa_selector: 'groups_list_tab' } do - %span - = _('Groups') - %span.badge.badge-pill= @group.shared_with_group_links.count - - if show_invited_members + %ul.nav-links.mobile-separator.nav.nav-tabs %li.nav-item - = link_to '#tab-invited-members', class: ['nav-link', ('active' if invited_active)], data: { toggle: 'tab' } do + = link_to '#tab-members', class: ['nav-link', ('active' unless invited_active)], data: { toggle: 'tab' } do %span - = _('Invited') - %span.badge.badge-pill= @invited_members.total_count - - if show_access_requests - %li.nav-item - = link_to '#tab-access-requests', class: 'nav-link', data: { toggle: 'tab' } do - %span - = _('Access requests') - %span.badge.badge-pill= @requesters.count - .tab-content - #tab-members.tab-pane{ class: ('active' unless invited_active) } - .card.card-without-border - - unless filtered_search_enabled - = render 'groups/group_members/tab_pane/header' do - = render 'groups/group_members/tab_pane/title' do - = html_escape(_('Members with access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe } - = form_tag group_group_members_path(@group), method: :get, class: 'user-search-form gl-display-flex gl-md-align-items-center gl-flex-wrap gl-flex-direction-column gl-md-flex-direction-row gl-mx-n3 gl-my-n3', data: { testid: 'user-search-form' } do - .gl-px-3.gl-py-2 - .search-control-wrap.gl-relative - = render 'shared/members/search_field' - - if can_manage_members - = render 'groups/group_members/tab_pane/form_item' do - = label_tag '2fa', _('2FA'), class: form_item_label_css_class - = render 'shared/members/filter_2fa_dropdown' - = render 'groups/group_members/tab_pane/form_item' do - = label_tag :sort_by, _('Sort by'), class: form_item_label_css_class - = render 'shared/members/sort_dropdown' - - if vue_members_list_enabled - .js-group-members-list{ data: group_members_list_data_attributes(@group, @members) } + = _('Members') + %span.badge.badge-pill= @members.total_count + - if @group.shared_with_group_links.any? + %li.nav-item + = link_to '#tab-groups', class: ['nav-link'] , data: { toggle: 'tab', qa_selector: 'groups_list_tab' } do + %span + = _('Groups') + %span.badge.badge-pill= @group.shared_with_group_links.count + - if show_invited_members + %li.nav-item + = link_to '#tab-invited-members', class: ['nav-link', ('active' if invited_active)], data: { toggle: 'tab' } do + %span + = _('Invited') + %span.badge.badge-pill= @invited_members.total_count + - if show_access_requests + %li.nav-item + = link_to '#tab-access-requests', class: 'nav-link', data: { toggle: 'tab' } do + %span + = _('Access requests') + %span.badge.badge-pill= @requesters.count + .tab-content + #tab-members.tab-pane{ class: ('active' unless invited_active) } + .js-group-members-list{ data: group_members_list_data_attributes(@group, @members) } + .loading + .spinner.spinner-md + = paginate @members, theme: 'gitlab', params: { invited_members_page: nil, search_invited: nil } + - if @group.shared_with_group_links.any? + #tab-groups.tab-pane + .js-group-linked-list{ data: linked_groups_list_data_attributes(@group) } + .loading + .spinner.spinner-md + - if show_invited_members + #tab-invited-members.tab-pane{ class: ('active' if invited_active) } + .js-group-invited-members-list{ data: group_members_list_data_attributes(@group, @invited_members) } .loading .spinner.spinner-md - - else - %ul.content-list.members-list{ data: { qa_selector: 'members_list' } } - = render partial: 'shared/members/member', - collection: @members, as: :member, - locals: { membership_source: @group, - group: @group, - current_user_is_group_owner: current_user_is_group_owner } - = paginate @members, theme: 'gitlab', params: { invited_members_page: nil, search_invited: nil } - - if @group.shared_with_group_links.any? - #tab-groups.tab-pane - .card.card-without-border - - unless filtered_search_enabled - = render 'groups/group_members/tab_pane/header' do - = render 'groups/group_members/tab_pane/title' do - = html_escape(_('Groups with access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe } - - if vue_members_list_enabled - .js-group-linked-list{ data: linked_groups_list_data_attributes(@group) } - .loading - .spinner.spinner-md - - else - %ul.content-list.members-list{ data: { qa_selector: 'groups_list' } } - - @group.shared_with_group_links.each do |group_link| - = render 'shared/members/group', group_link: group_link, can_admin_member: can_manage_members, group_link_path: group_group_link_path(@group, group_link) - - if show_invited_members - #tab-invited-members.tab-pane{ class: ('active' if invited_active) } - .card.card-without-border - - unless filtered_search_enabled - = render 'groups/group_members/tab_pane/header' do - = render 'groups/group_members/tab_pane/title' do - = html_escape(_('Members invited to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe } - = form_tag group_group_members_path(@group), method: :get, class: 'user-search-form', data: { testid: 'user-search-form' } do - = render 'shared/members/search_field', name: 'search_invited' - - if vue_members_list_enabled - .js-group-invited-members-list{ data: group_members_list_data_attributes(@group, @invited_members) } - .loading - .spinner.spinner-md - - else - %ul.content-list.members-list - = render partial: 'shared/members/member', - collection: @invited_members, as: :member, - locals: { membership_source: @group, - group: @group, - current_user_is_group_owner: current_user_is_group_owner } = paginate @invited_members, param_name: 'invited_members_page', theme: 'gitlab', params: { page: nil } - - if show_access_requests - #tab-access-requests.tab-pane - .card.card-without-border - - unless filtered_search_enabled - = render 'groups/group_members/tab_pane/header' do - = render 'groups/group_members/tab_pane/title' do - = html_escape(_('Users requesting access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe } - - if vue_members_list_enabled - .js-group-access-requests-list{ data: group_members_list_data_attributes(@group, @requesters) } - .loading - .spinner.spinner-md - - else - %ul.content-list.members-list - = render partial: 'shared/members/member', - collection: @requesters, as: :member, - locals: { membership_source: @group, - group: @group, - current_user_is_group_owner: current_user_is_group_owner } + - if show_access_requests + #tab-access-requests.tab-pane + .js-group-access-requests-list{ data: group_members_list_data_attributes(@group, @requesters) } + .loading + .spinner.spinner-md diff --git a/app/views/groups/registry/repositories/index.html.haml b/app/views/groups/registry/repositories/index.html.haml index e26b8317c1c..6d0a3e03019 100644 --- a/app/views/groups/registry/repositories/index.html.haml +++ b/app/views/groups/registry/repositories/index.html.haml @@ -1,21 +1,20 @@ - page_title _("Container Registry") - @content_class = "limit-container-width" unless fluid_layout +- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @group.full_path, first: 10, name: nil, isGroupPage: true} ) %section - .row.registry-placeholder.prepend-bottom-10 - .col-12 - #js-container-registry{ data: { endpoint: group_container_registries_path(@group), - "help_page_path" => help_page_path('user/packages/container_registry/index'), - "two_factor_auth_help_link" => help_page_path('user/profile/account/two_factor_authentication'), - "personal_access_tokens_help_link" => help_page_path('user/profile/personal_access_tokens'), - "no_containers_image" => image_path('illustrations/docker-empty-state.svg'), - "containers_error_image" => image_path('illustrations/docker-error-state.svg'), - "registry_host_url_with_port" => escape_once(registry_config.host_port), - "garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'), - "run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'), - "cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'), - "is_admin": current_user&.admin.to_s, - is_group_page: "true", - "group_path": @group.full_path, - "gid_prefix": container_repository_gid_prefix, - character_error: @character_error.to_s } } + #js-container-registry{ data: { endpoint: group_container_registries_path(@group), + "help_page_path" => help_page_path('user/packages/container_registry/index'), + "two_factor_auth_help_link" => help_page_path('user/profile/account/two_factor_authentication'), + "personal_access_tokens_help_link" => help_page_path('user/profile/personal_access_tokens'), + "no_containers_image" => image_path('illustrations/docker-empty-state.svg'), + "containers_error_image" => image_path('illustrations/docker-error-state.svg'), + "registry_host_url_with_port" => escape_once(registry_config.host_port), + "garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'), + "run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'), + "cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'), + "is_admin": current_user&.admin.to_s, + is_group_page: "true", + "group_path": @group.full_path, + "gid_prefix": container_repository_gid_prefix, + character_error: @character_error.to_s } } diff --git a/app/views/groups/runners/_group_runners.html.haml b/app/views/groups/runners/_group_runners.html.haml index 554240b7aef..944ef3435c1 100644 --- a/app/views/groups/runners/_group_runners.html.haml +++ b/app/views/groups/runners/_group_runners.html.haml @@ -1,11 +1,11 @@ -- link = link_to _('Runners API'), help_page_path('api/runners.md') +- link = link_to _('Runner API'), help_page_path('api/runners.md') -%h3 - = _('Group Runners') +%h4 + = _('Group runners') -.bs-callout.bs-callout-warning - = _('GitLab Group Runners can execute code for all the projects in this group.') - = _('They can be managed using the %{link}.').html_safe % { link: link } +%p + = _('These runners are shared across projects in this group.') + = _('Group runners can be managed with the %{link}.').html_safe % { link: link } -# Proper policies should be implemented per -# https://gitlab.com/gitlab-org/gitlab-foss/issues/45894 @@ -18,3 +18,4 @@ locals: { registration_token: @group.runners_token, type: 'group', reset_token_url: reset_registration_token_group_settings_ci_cd_path } + %br diff --git a/app/views/groups/runners/_index.html.haml b/app/views/groups/runners/_index.html.haml index b342b589d93..7cbc709ecf8 100644 --- a/app/views/groups/runners/_index.html.haml +++ b/app/views/groups/runners/_index.html.haml @@ -2,8 +2,6 @@ %hr -%p.lead - = _('To start serving your jobs you can add Runners to your group') .row .col-sm-6 = render 'groups/runners/group_runners' @@ -11,7 +9,7 @@ = render 'groups/runners/shared_runners' %h4.underlined-title - = _('Available Runners: %{runners}').html_safe % { runners: limited_counter_with_delimiter(@all_group_runners) } + = _('Available runners: %{runners}').html_safe % { runners: limited_counter_with_delimiter(@all_group_runners) } -# haml-lint:disable NoPlainNodes .row diff --git a/app/views/groups/settings/_two_factor_auth.html.haml b/app/views/groups/settings/_two_factor_auth.html.haml index d2d4c27c826..fac3df5237f 100644 --- a/app/views/groups/settings/_two_factor_auth.html.haml +++ b/app/views/groups/settings/_two_factor_auth.html.haml @@ -18,6 +18,6 @@ - unless group.has_parent? .form-group .form-check - = f.check_box :allow_mfa_for_subgroups, class: 'form-check-input', checked: group.namespace_settings.allow_mfa_for_subgroups + = f.check_box :allow_mfa_for_subgroups, class: 'form-check-input', checked: group.namespace_settings&.allow_mfa_for_subgroups = f.label :allow_mfa_for_subgroups, class: 'form-check-label' do = _('Allow subgroups to set up their own two-factor authentication rules') diff --git a/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml b/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml index 2b5019222f8..8f1ce11ce40 100644 --- a/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml +++ b/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml @@ -10,6 +10,6 @@ %strong= s_('GroupSettings|Default to Auto DevOps pipeline for all projects within this group') %span.badge.badge-info#auto-devops-badge= badge_for_auto_devops_scope(group) .form-text.text-muted - = s_('GroupSettings|The Auto DevOps pipeline will run if no alternative CI configuration file is found.') - = link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank' + = s_('GroupSettings|The Auto DevOps pipeline runs if no alternative CI configuration file is found.') + = link_to _('Learn more.'), help_page_path('topics/autodevops/index.md'), target: '_blank' = f.submit _('Save changes'), class: 'btn btn-success gl-mt-5' diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml index 5b5f357dbec..4a0a92fa91f 100644 --- a/app/views/groups/settings/ci_cd/show.html.haml +++ b/app/views/groups/settings/ci_cd/show.html.haml @@ -31,8 +31,8 @@ %button.btn.gl-button.btn-default.js-settings-toggle{ type: "button" } = expanded ? _('Collapse') : _('Expand') %p - = _("Runners are processes that pick up and execute jobs for GitLab. Here you can register and see your Runners for this project.") - = link_to s_('More information'), help_page_path('ci/runners/README') + = _("Runners are processes that pick up and execute CI/CD jobs for GitLab.") + = link_to s_('How do I configure runners?'), help_page_path('ci/runners/README') .settings-content = render 'groups/runners/index' @@ -47,7 +47,7 @@ - quickstart_url = help_page_path('topics/autodevops/quick_start_guide') - auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url } - quickstart_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: quickstart_url } - = s_('AutoDevOps|Auto DevOps can automatically build, test, and deploy applications based on predefined continuous integration and delivery configuration. %{auto_devops_start}Learn more about Auto DevOps%{auto_devops_end} or use our %{quickstart_start}quick start guide%{quickstart_end} to get started right away.').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe, quickstart_start: quickstart_start, quickstart_end: '</a>'.html_safe } + = s_('AutoDevOps|%{auto_devops_start}Automate building, testing, and deploying%{auto_devops_end} your applications based on your continuous integration and delivery configuration. %{quickstart_start}How do I get started?%{quickstart_end}').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe, quickstart_start: quickstart_start, quickstart_end: '</a>'.html_safe } .settings-content = render 'groups/settings/ci_cd/auto_devops_form', group: @group diff --git a/app/views/groups/settings/packages_and_registries/index.html.haml b/app/views/groups/settings/packages_and_registries/index.html.haml new file mode 100644 index 00000000000..33719d56af1 --- /dev/null +++ b/app/views/groups/settings/packages_and_registries/index.html.haml @@ -0,0 +1,5 @@ +- breadcrumb_title _('Packages & Registries') +- page_title _('Packages & Registries') +- @content_class = 'limit-container-width' unless fluid_layout + +%section#js-packages-and-registries-settings diff --git a/app/views/jira_connect/subscriptions/index.html.haml b/app/views/jira_connect/subscriptions/index.html.haml index b826a1b6fc6..ed765f80b74 100644 --- a/app/views/jira_connect/subscriptions/index.html.haml +++ b/app/views/jira_connect/subscriptions/index.html.haml @@ -9,10 +9,9 @@ = link_to _('Sign in to GitLab'), jira_connect_users_path, target: '_blank', rel: 'noopener noreferrer', class: 'js-jira-connect-sign-in' .jira-connect-app - %h1 - GitLab for Jira Configuration - - if current_user.blank? && @subscriptions.empty? + %h1 + GitLab for Jira Configuration %h2.heading-with-border Sign in to GitLab.com to get started. .gl-mt-5 @@ -21,17 +20,18 @@ .gl-mt-5 %p Note: this integration only works with accounts on GitLab.com (SaaS). - else - .js-jira-connect-app + .js-jira-connect-app{ data: jira_connect_app_data } - %form#add-subscription-form.subscription-form{ action: jira_connect_subscriptions_path } - .ak-field-group - %label - GitLab namespace + - unless new_jira_connect_ui? + %form#add-subscription-form.subscription-form{ action: jira_connect_subscriptions_path } + .ak-field-group + %label + GitLab namespace - .ak-field-group.field-group-input - %input#namespace-input.ak-field-text{ type: 'text', required: true, placeholder: 'e.g. "MyCompany" or "MyCompany/GroupName"' } - %button.ak-button.ak-button__appearance-primary{ type: 'submit' } - Link namespace to Jira + .ak-field-group.field-group-input + %input#namespace-input.ak-field-text{ type: 'text', required: true, placeholder: 'e.g. "MyCompany" or "MyCompany/GroupName"' } + %button.ak-button.ak-button__appearance-primary{ type: 'submit' } + Link namespace to Jira - if @subscriptions.present? %table.subscriptions @@ -49,6 +49,7 @@ - else %h4.empty-subscriptions No linked namespaces + %p= s_('Integrations|Namespaces are your GitLab groups and subgroups that will be linked to this Jira instance.') %p.browser-limitations-notice %strong Browser limitations: diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index bdd506ab3be..7aa57331c51 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -43,8 +43,6 @@ = stylesheet_link_tag_defer "application" = yield :page_specific_styles = stylesheet_link_tag_defer "application_utilities" - - unless use_startup_css? - = stylesheet_link_tag_defer "themes/#{user_application_theme_css_filename}" if user_application_theme_css_filename = stylesheet_link_tag "disable_animations", media: "all" if Rails.env.test? || Gitlab.config.gitlab['disable_animations'] = stylesheet_link_tag_defer "highlight/themes/#{user_color_scheme}" diff --git a/app/views/layouts/_startup_css.haml b/app/views/layouts/_startup_css.haml index 2f674f79b2f..35b91c8d35e 100644 --- a/app/views/layouts/_startup_css.haml +++ b/app/views/layouts/_startup_css.haml @@ -1,5 +1,3 @@ -- return unless use_startup_css? - - startup_filename = current_path?("sessions#new") ? 'signin' : user_application_theme == 'gl-dark' ? 'dark' : 'general' %style{ type: "text/css" } diff --git a/app/views/layouts/_startup_css_activation.haml b/app/views/layouts/_startup_css_activation.haml index 5fb53385acc..7dfb9cd1530 100644 --- a/app/views/layouts/_startup_css_activation.haml +++ b/app/views/layouts/_startup_css_activation.haml @@ -1,5 +1,3 @@ -- return unless use_startup_css? - = javascript_tag do :plain document.querySelectorAll('link[media="print"]').forEach(linkTag => { diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index 5daee24cb51..ef61a04c288 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -38,12 +38,4 @@ .col-sm-5.order-1.order-sm-12.new-session-forms-container = yield - %hr.footer-fixed - .container.footer-container - .footer-links - - if !public_visibility_restricted? - = link_to _("Explore"), explore_root_path - = link_to _("Help"), help_path - = link_to _("About GitLab"), "https://about.gitlab.com/" - - = footer_message + = render 'devise/shared/footer', footer_message: footer_message diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml index 6ac80a5aba3..b5649be8917 100644 --- a/app/views/layouts/devise_empty.html.haml +++ b/app/views/layouts/devise_empty.html.haml @@ -1,5 +1,5 @@ !!! 5 -%html{ lang: "en", class: system_message_class } +%html.devise-layout-html{ lang: "en", class: system_message_class } = render "layouts/head" %body.ui-indigo.login-page.application.navless{ class: "#{client_class_list}" } = header_message @@ -11,11 +11,4 @@ = render "layouts/flash" = yield - %hr - .container - .footer-links - - if !public_visibility_restricted? - = link_to _("Explore"), explore_root_path - = link_to _("Help"), help_path - = link_to _("About GitLab"), "https://about.gitlab.com/" - = footer_message + = render 'devise/shared/footer', footer_message: footer_message diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 70ab0a56581..f7e93182ca2 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -16,7 +16,7 @@ = logo_text - if Gitlab.com_and_canary? = link_to 'https://next.gitlab.com', class: 'canary-badge bg-transparent', target: :_blank do - %span.color-label.has-tooltip.badge.badge-pill.green-badge + %span.gl-badge.gl-bg-green-500.gl-text-white.gl-rounded-pill.gl-font-weight-bold.gl-py-1 = _('Next') - if current_user @@ -47,17 +47,36 @@ %span.badge.badge-pill.issues-count.green-badge{ class: ('hidden' if issues_count == 0) } = number_with_delimiter(issues_count) - if header_link?(:merge_requests) - = nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do - = link_to assigned_mrs_dashboard_path, title: _('Merge requests'), class: 'dashboard-shortcuts-merge_requests', aria: { label: _('Merge requests') }, - data: { qa_selector: 'merge_requests_shortcut_button', toggle: 'tooltip', placement: 'bottom', + - reviewers_enabled = merge_request_reviewers_enabled? + = nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter #{reviewers_enabled ? 'dropdown' : ''}" }) do + = link_to assigned_mrs_dashboard_path, class: 'dashboard-shortcuts-merge_requests', title: _('Merge requests'), aria: { label: _('Merge requests') }, + data: { qa_selector: 'merge_requests_shortcut_button', + toggle: reviewers_enabled ? "dropdown" : "tooltip", + placement: 'bottom', track_label: 'main_navigation', track_event: 'click_merge_link', track_property: 'navigation', container: 'body' } do = sprite_icon('git-merge') - - merge_requests_count = assigned_issuables_count(:merge_requests) - %span.badge.badge-pill.merge-requests-count{ class: ('hidden' if merge_requests_count == 0) } - = number_with_delimiter(merge_requests_count) + %span.badge.badge-pill.merge-requests-count.js-merge-requests-count{ class: ('hidden' if user_merge_requests_counts[:total] == 0) } + = number_with_delimiter(user_merge_requests_counts[:total]) + - if reviewers_enabled + = sprite_icon('chevron-down', css_class: 'caret-down gl-mx-0!') + - if reviewers_enabled + .dropdown-menu.dropdown-menu-right + %ul + %li.dropdown-header + = _('Merge requests') + %li + = link_to assigned_mrs_dashboard_path, class: 'gl-display-flex! gl-align-items-center' do + = _('Assigned to you') + %span.badge.gl-badge.badge-pill.badge-muted.merge-request-badge.gl-ml-auto.js-assigned-mr-count{ class: "" } + = user_merge_requests_counts[:assigned] + %li + = link_to reviewer_mrs_dashboard_path, class: 'gl-display-flex! gl-align-items-center' do + = _('Review requests for you') + %span.badge.gl-badge.badge-pill.badge-muted.merge-request-badge.gl-ml-auto.js-reviewer-mr-count{ class: "" } + = user_merge_requests_counts[:review_requested] - if header_link?(:todos) = nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do = link_to dashboard_todos_path, title: _('To-Do List'), aria: { label: _('To-Do List') }, class: 'shortcuts-todos', diff --git a/app/views/layouts/header/_group_invite_members_new_dropdown_item.html.haml b/app/views/layouts/header/_group_invite_members_new_dropdown_item.html.haml new file mode 100644 index 00000000000..cb74c77dff8 --- /dev/null +++ b/app/views/layouts/header/_group_invite_members_new_dropdown_item.html.haml @@ -0,0 +1,3 @@ +- return unless Gitlab::Experimentation.active?(:invite_members_new_dropdown) && can?(current_user, :admin_group_member, @group) + +%li= dropdown_invite_members_link(@group) diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index 2c5cd7e96c7..1fc9831d271 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -2,7 +2,7 @@ = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", id: "js-onboarding-new-project-link", title: _("New..."), ref: 'tooltip', aria: { label: _("New...") }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static' } do = sprite_icon('plus-square') = sprite_icon('chevron-down', css_class: 'caret-down') - .dropdown-menu.dropdown-menu-right + .dropdown-menu.dropdown-menu-right.dropdown-extended-height %ul - if @group&.persisted? - create_group_project = can?(current_user, :create_projects, @group) @@ -16,6 +16,7 @@ - if create_group_subgroup %li= link_to _('New subgroup'), new_group_path(parent_id: @group.id) = render_if_exists 'layouts/header/create_epic_new_dropdown_item' + = render 'layouts/header/group_invite_members_new_dropdown_item' %li.divider %li.dropdown-bold-header GitLab @@ -33,6 +34,7 @@ %li= link_to _('New merge request'), project_new_merge_request_path(merge_project) - if create_project_snippet %li= link_to _('New snippet'), new_project_snippet_path(@project) + = render 'layouts/header/project_invite_members_new_dropdown_item' %li.divider %li.dropdown-bold-header GitLab - if current_user.can_create_project? diff --git a/app/views/layouts/header/_project_invite_members_new_dropdown_item.html.haml b/app/views/layouts/header/_project_invite_members_new_dropdown_item.html.haml new file mode 100644 index 00000000000..2cb67e857e3 --- /dev/null +++ b/app/views/layouts/header/_project_invite_members_new_dropdown_item.html.haml @@ -0,0 +1,3 @@ +- return unless Gitlab::Experimentation.active?(:invite_members_new_dropdown) && can_import_members? + +%li= dropdown_invite_members_link(@project) diff --git a/app/views/layouts/jira_connect.html.haml b/app/views/layouts/jira_connect.html.haml index 0d4ecfc5a10..d996b3387a3 100644 --- a/app/views/layouts/jira_connect.html.haml +++ b/app/views/layouts/jira_connect.html.haml @@ -3,8 +3,9 @@ %meta{ content: "text/html; charset=utf-8", "http-equiv" => "Content-Type" } %title GitLab - = stylesheet_link_tag 'https://unpkg.com/@atlaskit/css-reset@3.0.6/dist/bundle.css' - = stylesheet_link_tag 'https://unpkg.com/@atlaskit/reduced-ui-pack@10.5.5/dist/bundle.css' + - unless new_jira_connect_ui? + = stylesheet_link_tag 'https://unpkg.com/@atlaskit/css-reset@3.0.6/dist/bundle.css' + = stylesheet_link_tag 'https://unpkg.com/@atlaskit/reduced-ui-pack@10.5.5/dist/bundle.css' = yield :page_specific_styles = javascript_include_tag 'https://connect-cdn.atl-paas.net/all.js' diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml index 43f1011a85b..dd2c5e2a19e 100644 --- a/app/views/layouts/nav/_breadcrumbs.html.haml +++ b/app/views/layouts/nav/_breadcrumbs.html.haml @@ -8,7 +8,7 @@ - if defined?(@left_sidebar) = button_tag class: 'toggle-mobile-nav', type: 'button' do %span.sr-only= _("Open sidebar") - = sprite_icon('hamburger') + = sprite_icon('hamburger', size: 18) .breadcrumbs-links.js-title-container{ data: { qa_selector: 'breadcrumb_links_content' } } %ul.list-unstyled.breadcrumbs-list.js-breadcrumbs-list - unless hide_top_links diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index efe8e57cadf..473a0d131b8 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -10,6 +10,8 @@ .sidebar-context-title = @group.name %ul.sidebar-top-level-items.qa-group-sidebar + = render_if_exists 'layouts/nav/sidebar/group_trial_status_widget', group: @group + - if group_sidebar_link?(:overview) - paths = group_overview_nav_link_paths = nav_link(path: paths, unless: -> { current_path?('groups/contribution_analytics#show') }, html_options: { class: 'home' }) do @@ -148,7 +150,7 @@ = sprite_icon('settings') %span.nav-item-name.qa-group-settings-item = _('Settings') - %ul.sidebar-sub-level-items.qa-group-sidebar-submenu + %ul.sidebar-sub-level-items.qa-group-sidebar-submenu{ data: { testid: 'group-settings-menu' } } = nav_link(path: %w[groups#projects groups#edit badges#index ci_cd#show], html_options: { class: "fly-out-top-item" } ) do = link_to edit_group_path(@group) do %strong.fly-out-top-item-name @@ -179,6 +181,12 @@ %span = _('CI / CD') + - if Feature.enabled?(:packages_and_registries_group_settings, @group) + = nav_link(controller: :packages_and_registries) do + = link_to group_settings_packages_and_registries_path(@group), title: _('Packages & Registries') do + %span + = _('Packages & Registries') + = render_if_exists "groups/ee/settings_nav" = render_if_exists "groups/ee/administration_nav" diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 5cadabd5f90..e02b8333c60 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -300,8 +300,8 @@ = link_to _('Auto DevOps'), help_page_path('topics/autodevops/index.md') %span= _('uses Kubernetes clusters to deploy your code!') %hr - %button.btn.btn-success.btn-sm.dismiss-feature-highlight{ type: 'button' } - %span= _("Got it!") + %button.gl-button.btn.btn-success.btn-sm.dismiss-feature-highlight{ type: 'button' } + %span.gl-mr-2= _("Got it!") = sprite_icon('thumb-up') - if project_nav_tab? :environments diff --git a/app/views/notify/_issuable_csv_export.html.haml b/app/views/notify/_issuable_csv_export.html.haml index 5a581811179..3b1fe90eaee 100644 --- a/app/views/notify/_issuable_csv_export.html.haml +++ b/app/views/notify/_issuable_csv_export.html.haml @@ -3,4 +3,4 @@ = _('Your CSV export of %{count} from project %{project_link} has been added to this email as an attachment.').html_safe % { count: pluralize(@written_count, type.to_s.titleize.downcase), project_link: project_link } - if @truncated %p - = _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{count} issues have been included. Consider re-exporting with a narrower selection of issues.') % { written_count: @written_count, count: @count } + = _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of %{size_limit}. %{written_count} of %{count} %{issuables} have been included. Consider re-exporting with a narrower selection of %{issuables}.') % { written_count: @written_count, count: @count, issuables: type.to_s.pluralize, size_limit: @size_limit } diff --git a/app/views/notify/issue_due_email.html.haml b/app/views/notify/issue_due_email.html.haml index 08bc98ca05c..adb9da05694 100644 --- a/app/views/notify/issue_due_email.html.haml +++ b/app/views/notify/issue_due_email.html.haml @@ -1,5 +1,5 @@ %p.details - #{link_to @issue.author_name, user_url(@issue.author)}'s issue is due soon. + #{link_to @issue.author_name, user_url(@issue.author)}'s issue #{issue_reference_link(@issue)} is due soon. - if @issue.assignees.any? %p diff --git a/app/views/notify/issue_due_email.text.erb b/app/views/notify/issue_due_email.text.erb index ae50b703fe3..e5bfcc70355 100644 --- a/app/views/notify/issue_due_email.text.erb +++ b/app/views/notify/issue_due_email.text.erb @@ -1,6 +1,6 @@ The following issue is due on <%= @issue.due_date %>: -Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %> +Issue <%= @issue.iid %>: <%= issue_reference_link(@issue) %> Author: <%= @issue.author_name %> <%= assignees_label(@issue) %> diff --git a/app/views/notify/issues_csv_email.text.erb b/app/views/notify/issues_csv_email.text.erb index a1d2a4691bc..cf2910c4014 100644 --- a/app/views/notify/issues_csv_email.text.erb +++ b/app/views/notify/issues_csv_email.text.erb @@ -1,5 +1,5 @@ <%= _('Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment.') % { written_count: pluralize(@written_count, 'issue'), project_name: @project.full_name, project_url: project_url(@project) } %> <% if @truncated %> - <%= _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues.') % { written_count: @written_count, issues_count: @issues_count} %> + <%= _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of %{size_limit}. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues.') % { written_count: @written_count, issues_count: @issues_count, size_limit: @size_limit } %> <% end %> diff --git a/app/views/notify/member_expiration_date_updated_email.html.haml b/app/views/notify/member_expiration_date_updated_email.html.haml new file mode 100644 index 00000000000..6c4db22eeaa --- /dev/null +++ b/app/views/notify/member_expiration_date_updated_email.html.haml @@ -0,0 +1,6 @@ += email_default_heading(say_hi(@member.user)) + +%p + = group_membership_expiration_changed_text(@member, @member_source) +%p + = group_membership_expiration_changed_link(@member, @member_source, format: :html) diff --git a/app/views/notify/member_expiration_date_updated_email.text.erb b/app/views/notify/member_expiration_date_updated_email.text.erb new file mode 100644 index 00000000000..8b3a5a55e77 --- /dev/null +++ b/app/views/notify/member_expiration_date_updated_email.text.erb @@ -0,0 +1,5 @@ +<%= say_hi(@member.user) %> + +<%= group_membership_expiration_changed_text(@member, @member_source) %> + +<%= group_membership_expiration_changed_link(@member, @member_source) %> diff --git a/app/views/notify/merge_requests_csv_email.text.erb b/app/views/notify/merge_requests_csv_email.text.erb index 9ed971bbe9c..78d11dde69f 100644 --- a/app/views/notify/merge_requests_csv_email.text.erb +++ b/app/views/notify/merge_requests_csv_email.text.erb @@ -1,5 +1,5 @@ <%= _('Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment.') % { written_count: pluralize(@written_count, 'merge request'), project_name: @project.full_name, project_url: project_url(@project) } %> <% if @truncated %> - <%= _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{merge_requests_count} issues have been included. Consider re-exporting with a narrower selection of issues.') % { written_count: @written_count, merge_requests_count: @merge_requests_count} %> + <%= _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of %{size_limit}. %{written_count} of %{merge_requests_count} merge requests have been included. Consider re-exporting with a narrower selection of merge requests.') % { written_count: @written_count, merge_requests_count: @merge_requests_count, size_limit: @size_limit} %> <% end %> diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml index 575ec8c488e..4e8d8a20ef1 100644 --- a/app/views/notify/pipeline_failed_email.html.haml +++ b/app/views/notify/pipeline_failed_email.html.haml @@ -6,7 +6,7 @@ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;line-height:1;" } %img{ alt: "ā", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" } - Your pipeline has failed. + Pipeline ##{@pipeline.id} has failed! %tr.spacer %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb index a30e331d892..2deca375db1 100644 --- a/app/views/notify/pipeline_failed_email.text.erb +++ b/app/views/notify/pipeline_failed_email.text.erb @@ -1,4 +1,4 @@ -Your pipeline has failed. +Pipeline #<%= @pipeline.id %> has failed! Project: <%= @project.name %> ( <%= project_url(@project) %> ) Branch: <%= @pipeline.source_ref %> ( <%= commits_url(@pipeline) %> ) diff --git a/app/views/notify/pipeline_fixed_email.html.haml b/app/views/notify/pipeline_fixed_email.html.haml index 05c0027a6fc..f2dbb3b20b7 100644 --- a/app/views/notify/pipeline_fixed_email.html.haml +++ b/app/views/notify/pipeline_fixed_email.html.haml @@ -1 +1 @@ -= render 'notify/successful_pipeline', title: 'Your pipeline has been fixed!' += render 'notify/successful_pipeline', title: "Pipeline has been fixed and ##{@pipeline.id} has passed!" diff --git a/app/views/notify/pipeline_fixed_email.text.erb b/app/views/notify/pipeline_fixed_email.text.erb index 75268531bdc..32334260a5e 100644 --- a/app/views/notify/pipeline_fixed_email.text.erb +++ b/app/views/notify/pipeline_fixed_email.text.erb @@ -1 +1 @@ -<%= render 'notify/successful_pipeline', title: 'Your pipeline has been fixed!' -%> +<%= render 'notify/successful_pipeline', title: "Pipeline has been fixed and ##{@pipeline.id} has passed!" -%> diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml index c34e02b5fee..47832907663 100644 --- a/app/views/notify/pipeline_success_email.html.haml +++ b/app/views/notify/pipeline_success_email.html.haml @@ -1 +1 @@ -= render 'notify/successful_pipeline', title: 'Your pipeline has passed.' += render 'notify/successful_pipeline', title: "Pipeline ##{@pipeline.id} has passed!" diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb index b554bffc908..83cdb72d252 100644 --- a/app/views/notify/pipeline_success_email.text.erb +++ b/app/views/notify/pipeline_success_email.text.erb @@ -1 +1 @@ -<%= render 'notify/successful_pipeline', title: 'Your pipeline has passed.' -%> +<%= render 'notify/successful_pipeline', title: "Pipeline ##{@pipeline.id} has passed!" -%> diff --git a/app/views/notify/provisioned_member_access_granted_email.erb b/app/views/notify/provisioned_member_access_granted_email.erb new file mode 100644 index 00000000000..485ee5a5242 --- /dev/null +++ b/app/views/notify/provisioned_member_access_granted_email.erb @@ -0,0 +1,14 @@ +<% source_link = member_source.web_url %> + +<%= _('An Enterprise User GitLab account has been created for you by your organization:') %> +<%= _('Username: %{username}') % { username: @user.username } %> +<%= _('Email: %{email}') % { email: @user.email } %> +<%= _('GitLab group: %{source_link}').html_safe % { source_link: source_link } %> + + +<%= _('By authenticating with an account tied to an Enterprise e-mail address, it is understood that this account is an Enterprise User. ') %> +<%= _('To ensure no loss of personal content, an Individual User should create a separate account under their own personal email address, not tied to the Enterprise email domain or name-space.') %> +<%- unless @user.confirmed? %> + <%= _('To get started, click the link below to confirm your account.') %> + <%= confirmation_url(@user, confirmation_token: @user.confirmation_token) %> +<%- end %> diff --git a/app/views/notify/provisioned_member_access_granted_email.haml b/app/views/notify/provisioned_member_access_granted_email.haml new file mode 100644 index 00000000000..2f2fd33145a --- /dev/null +++ b/app/views/notify/provisioned_member_access_granted_email.haml @@ -0,0 +1,24 @@ +- source_link = link_to(member_source.human_name, member_source.web_url, target: '_blank', rel: 'noopener noreferrer', class: :highlight) +- confirmation_link = confirmation_url(@user, confirmation_token: @user.confirmation_token) + +%tr + %td.text-content + %p + = _('An Enterprise User GitLab account has been created for you by your organization:') + %p + = _('Username: %{username}') % { username: @user.username } + %br + = _('Email: %{email}') % { email: @user.email } + %br + = _('GitLab group: %{source_link}').html_safe % { source_link: source_link } + +%tr + %td.text-content + %p + = _('By authenticating with an account tied to an Enterprise e-mail address, it is understood that this account is an Enterprise User. ') + = _('To ensure no loss of personal content, an Individual User should create a separate account under their own personal email address, not tied to the Enterprise email domain or name-space.') + - unless @user.confirmed? + %p + = _('To get started, click the link below to confirm your account.') + %p + = link_to 'Confirm your account', confirmation_link diff --git a/app/views/profiles/chat_names/index.html.haml b/app/views/profiles/chat_names/index.html.haml index e0b0f839455..782850afcda 100644 --- a/app/views/profiles/chat_names/index.html.haml +++ b/app/views/profiles/chat_names/index.html.haml @@ -9,7 +9,7 @@ = _('You can see your chat accounts.') .col-lg-8 - %h5 Active chat names (#{@chat_names.size}) + %h5.gl-mt-0 Active chat names (#{@chat_names.size}) - if @chat_names.present? .table-responsive diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index bf9f1336a4f..41699d6f01f 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -70,7 +70,7 @@ prepend: emoji_button, append: reset_message_button, placeholder: s_("Profiles|What's your status?") - - if Feature.enabled?(:set_user_availability_status, @user) + - if Feature.enabled?(:set_user_availability_status, @user, default_enabled: :yaml) .checkbox-icon-inline-wrapper = status_form.check_box :availability, { data: { testid: "user-availability-checkbox" }, label: s_("Profiles|Busy"), wrapper_class: 'gl-mr-0 gl-font-weight-bold' }, availability["busy"], availability["not_set"] .gl-text-gray-600.gl-ml-5= s_('Profiles|"Busy" will be shown next to your name') diff --git a/app/views/profiles/two_factor_auths/_codes.html.haml b/app/views/profiles/two_factor_auths/_codes.html.haml index 178a9d3f8b4..9f850842f58 100644 --- a/app/views/profiles/two_factor_auths/_codes.html.haml +++ b/app/views/profiles/two_factor_auths/_codes.html.haml @@ -1,18 +1,3 @@ - show_success_alert = local_assigns.fetch(:show_success_alert, nil) -- if Feature.enabled?(:vue_2fa_recovery_codes, current_user, default_enabled: true) - .js-2fa-recovery-codes{ data: { codes: @codes.to_json, profile_account_path: profile_account_path(two_factor_auth_enabled_successfully: show_success_alert) } } -- else - %p.slead - - lose_2fa_message = _('Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one time each to regain access to your account. Please save them in a safe place, or you %{b_start}will%{b_end} lose access to your account.') % { b_start:'<b>', b_end:'</b>' } - = lose_2fa_message.html_safe - - .codes.card{ data: { qa_selector: 'codes_content' } } - %ul - - @codes.each do |code| - %li - %span.monospace{ data: { qa_selector: 'code_content' } }= code - - .d-flex - = link_to _('Proceed'), profile_account_path, class: 'gl-button btn btn-success gl-mr-3', data: { qa_selector: 'proceed_button' } - = link_to _('Download codes'), "data:text/plain;charset=utf-8,#{CGI.escape(@codes.join("\n"))}", download: "gitlab-recovery-codes.txt", class: 'gl-button btn btn-default' +.js-2fa-recovery-codes{ data: { codes: @codes.to_json, profile_account_path: profile_account_path(two_factor_auth_enabled_successfully: show_success_alert) } } diff --git a/app/views/profiles/two_factor_auths/create.html.haml b/app/views/profiles/two_factor_auths/create.html.haml index be4800024cf..606dda5ed55 100644 --- a/app/views/profiles/two_factor_auths/create.html.haml +++ b/app/views/profiles/two_factor_auths/create.html.haml @@ -1,8 +1,4 @@ - page_title _('Two-factor Authentication'), _('Account') - add_page_specific_style 'page_bundles/profile_two_factor_auth' -- unless Feature.enabled?(:vue_2fa_recovery_codes, current_user, default_enabled: true) - .gl-alert.gl-alert-success.gl-mb-5 - = _('Congratulations! You have enabled Two-factor Authentication!') - = render 'codes', show_success_alert: true diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml index 5ec2dc57f96..86dfcda6d1b 100644 --- a/app/views/projects/_export.html.haml +++ b/app/views/projects/_export.html.haml @@ -2,7 +2,7 @@ - project = local_assigns.fetch(:project) -.sub-section +.sub-section{ data: { qa_selector: 'export_project_content' } } %h4= _('Export project') %p= _('Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.') diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index ebb0dd8b39f..3e1d08e646e 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -25,7 +25,7 @@ %span.access-request-links.gl-ml-3 = render 'shared/members/access_request_links', source: @project - if @project.tag_list.present? - %span.home-panel-topic-list.mt-2.w-100.d-inline-flex.gl-font-base.gl-font-weight-normal + %span.home-panel-topic-list.mt-2.w-100.d-inline-flex.gl-font-base.gl-font-weight-normal.gl-align-items-center = sprite_icon('tag', css_class: 'icon gl-relative gl-mr-2') - @project.topics_to_show.each do |topic| diff --git a/app/views/projects/_invite_members_modal.html.haml b/app/views/projects/_invite_members_modal.html.haml index ad95f39bbfa..e8f61336882 100644 --- a/app/views/projects/_invite_members_modal.html.haml +++ b/app/views/projects/_invite_members_modal.html.haml @@ -1,7 +1,7 @@ - if invite_members_allowed?(project.group) .js-invite-members-modal{ data: { id: project.id, name: project.name, - is_project: true, + is_project: 'true', access_levels: GroupMember.access_level_roles.to_json, default_access_level: Gitlab::Access::GUEST, help_link: help_page_url('user/permissions') } } diff --git a/app/views/projects/_service_desk_settings.html.haml b/app/views/projects/_service_desk_settings.html.haml index 3b2b3a2ba67..153235c37d2 100644 --- a/app/views/projects/_service_desk_settings.html.haml +++ b/app/views/projects/_service_desk_settings.html.haml @@ -2,17 +2,17 @@ %section.settings.js-service-desk-setting-wrapper.no-animate#js-service-desk{ class: ('expanded' if expanded) } .settings-header %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Service Desk') - %button.btn.js-settings-toggle + %button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' } = expanded ? _('Collapse') : _('Expand') - link_start = "<a href='#{help_page_path('user/project/service_desk')}' target='_blank' rel='noopener noreferrer'>".html_safe - %p= _('Enable/disable your service desk. %{link_start}Learn more about service desk%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } + %p= _('Enable and disable Service Desk. Some additional configuration might be required. %{link_start}Learn more%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } .settings-content - if ::Gitlab::ServiceDesk.supported? .js-service-desk-setting-root{ data: { endpoint: project_service_desk_path(@project), enabled: "#{@project.service_desk_enabled}", incoming_email: (@project.service_desk_incoming_address if @project.service_desk_enabled), custom_email: (@project.service_desk_custom_address if @project.service_desk_enabled), - custom_email_enabled: "#{@project.service_desk_custom_address_enabled?}", + custom_email_enabled: "#{Gitlab::ServiceDeskEmail.enabled?}", selected_template: "#{@project.service_desk_setting&.issue_template_key}", outgoing_name: "#{@project.service_desk_setting&.outgoing_name}", project_key: "#{@project.service_desk_setting&.project_key}", diff --git a/app/views/projects/_visibility_modal.html.haml b/app/views/projects/_visibility_modal.html.haml index 314211057f9..182164d2175 100644 --- a/app/views/projects/_visibility_modal.html.haml +++ b/app/views/projects/_visibility_modal.html.haml @@ -18,9 +18,9 @@ = _('Once you confirm and press "Reduce project visibility":') %ul %li - = ("Current forks will keep their visibility level but their fork relationship with this project will be %{strong_start}removed%{strong_end}.").html_safe % { strong_start: strong_start, strong_end: strong_end } + = _("Current forks will keep their visibility level.").html_safe %label{ for: "confirm_path_input" } - = ("To confirm, type %{phrase_code}").html_safe % { phrase_code: '<code class="js-confirm-danger-match">%{phrase_name}</code>'.html_safe % { phrase_name: @project.full_path } } + = _("To confirm, type %{phrase_code}").html_safe % { phrase_code: '<code class="js-confirm-danger-match">%{phrase_name}</code>'.html_safe % { phrase_name: @project.full_path } } .form-group = text_field_tag 'confirm_path_input', '', class: 'form-control js-confirm-danger-input qa-confirm-input' .form-actions diff --git a/app/views/projects/alert_management/index.html.haml b/app/views/projects/alert_management/index.html.haml index 415820ac3ad..3bb489f2b69 100644 --- a/app/views/projects/alert_management/index.html.haml +++ b/app/views/projects/alert_management/index.html.haml @@ -1,3 +1,4 @@ - page_title _('Alerts') +- add_page_specific_style 'page_bundles/incident_management_list' #js-alert_management{ data: alert_management_data(@current_user, @project) } diff --git a/app/views/projects/blob/_template_selectors.html.haml b/app/views/projects/blob/_template_selectors.html.haml index b4962f4e78e..70a4202a5d0 100644 --- a/app/views/projects/blob/_template_selectors.html.haml +++ b/app/views/projects/blob/_template_selectors.html.haml @@ -11,5 +11,8 @@ = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-metrics-dashboard-selector qa-metrics-dashboard-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: metrics_dashboard_ymls(@project) } } ) #gitlab-ci-yml-selector.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitlab-ci-yml-selector qa-gitlab-ci-yml-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls(@project) } } ) + - if experiment_enabled?(:ci_syntax_templates, subject: current_user) + .gitlab-ci-syntax-yml-selector.js-gitlab-ci-syntax-yml-selector-wrap.js-template-selector-wrap.hidden + = dropdown_tag(_("Learn CI/CD syntax"), options: { toggle_class: 'js-gitlab-ci-syntax-yml-selector qa-gitlab-ci-syntax-yml-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_syntax_ymls(@project) } } ) .dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-dockerfile-selector qa-dockerfile-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: dockerfile_names(@project) } } ) diff --git a/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml b/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml index 3326cded42a..d8d27c3330b 100644 --- a/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml +++ b/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml @@ -1,9 +1,9 @@ - if viewer.valid?(project: @project, sha: @commit.sha, user: @current_user) = sprite_icon('check') - This GitLab CI configuration is valid. + = s_('Pipelines|This GitLab CI configuration is valid.') - else = sprite_icon('warning-solid') - This GitLab CI configuration is invalid: + = s_('Pipelines|This GitLab CI configuration is invalid:') = viewer.validation_message(project: @project, sha: @commit.sha, user: @current_user) -= link_to 'Learn more', help_page_path('ci/yaml/README') += link_to _('Learn more'), help_page_path('ci/yaml/README') diff --git a/app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml b/app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml index 379a6c3084a..b1c8e110493 100644 --- a/app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml +++ b/app/views/projects/blob/viewers/_gitlab_ci_yml_loading.html.haml @@ -1,4 +1,4 @@ = loading_icon(css_class: "gl-vertical-align-text-bottom mr-1") -Validating GitLab CI configurationā¦ += s_('Pipelines|Validating GitLab CI configurationā¦') -= link_to 'Learn more', help_page_path('ci/yaml/README') += link_to _('Learn more'), help_page_path('ci/yaml/README') diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 8f5fac1a40b..dc4172e2f09 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -4,7 +4,7 @@ %li{ class: "branch-item js-branch-item js-branch-#{branch.name}", data: { name: branch.name } } .branch-info .branch-title - = sprite_icon('fork', size: 12) + = sprite_icon('fork', size: 12, css_class: 'gl-flex-shrink-0') = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name gl-ml-3 qa-branch-name' do = branch.name - if branch.name == @repository.root_ref diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index 24dfb59dc85..17314cd7c5a 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -2,9 +2,12 @@ - default_ref = params[:ref] || @project.default_branch - if @error - .alert.alert-danger - %button.close{ type: "button", "data-dismiss" => "alert" } × - = @error + .gl-alert.gl-alert-danger + = sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title') + %button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') } + = sprite_icon('close', size: 16, css_class: 'gl-icon') + .gl-alert-body + = @error %h3.page-title New Branch %hr diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 8b4411776bc..017c804ced0 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -43,16 +43,16 @@ .label-container - if job.tags.any? - job.tags.each do |tag| - %span.badge.badge-primary + %span.badge.badge-pill.gl-badge.sm.badge-primary = tag - if job.try(:trigger_request) - %span.badge.badge-info= _('triggered') + %span.badge.badge-pill.gl-badge.sm.badge-info= _('triggered') - if job.try(:allow_failure) && !job.success? - %span.badge.badge-warning= _('allowed to fail') + %span.badge.badge-pill.gl-badge.sm.badge-warning= _('allowed to fail') - if job.schedulable? - %span.badge.badge-info= s_('DelayedJobs|delayed') + %span.badge.badge-pill.gl-badge.sm.badge-info= s_('DelayedJobs|delayed') - elsif job.action? - %span.badge.badge-info= _('manual') + %span.badge.badge-pill.gl-badge.sm.badge-info= _('manual') - if pipeline_link %td @@ -98,7 +98,7 @@ %td .gl-display-flex - - if can?(current_user, :read_build, job) && job.artifacts? + - if can?(current_user, :read_job_artifacts, job) && job.artifacts? = link_to download_project_job_artifacts_path(job.project, job), rel: 'nofollow', download: '', title: _('Download artifacts'), class: 'btn btn-build gl-button btn-icon btn-svg' do = sprite_icon('download') - if can?(current_user, :update_build, job) diff --git a/app/views/projects/ci/lints/show.html.haml b/app/views/projects/ci/lints/show.html.haml index feccea6cfc0..4463220e951 100644 --- a/app/views/projects/ci/lints/show.html.haml +++ b/app/views/projects/ci/lints/show.html.haml @@ -1,6 +1,6 @@ - page_title _("CI Lint") - page_description _("Validate your GitLab CI configuration file") -%h2.pt-3.pb-3= _("Validate your GitLab CI configuration") +%h4.pt-3.pb-3= _("Validate your GitLab CI configuration") #js-ci-lint{ data: { endpoint: project_ci_lint_path(@project), pipeline_simulation_help_page_path: help_page_path('ci/lint', anchor: 'pipeline-simulation') , lint_help_page_path: help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax') } } diff --git a/app/views/projects/ci/pipeline_editor/show.html.haml b/app/views/projects/ci/pipeline_editor/show.html.haml index f1f8658fa3b..10aed15f380 100644 --- a/app/views/projects/ci/pipeline_editor/show.html.haml +++ b/app/views/projects/ci/pipeline_editor/show.html.haml @@ -1,8 +1,12 @@ - page_title s_('Pipelines|Pipeline Editor') #js-pipeline-editor{ data: { "ci-config-path": @project.ci_config_path_or_default, - "project-path" => @project.full_path, + "project-path" => @project.path, + "project-full-path" => @project.full_path, + "project-namespace" => @project.namespace.full_path, "default-branch" => @project.default_branch, - "commit-id" => @project.commit ? @project.commit.id : '', + "commit-sha" => @project.commit ? @project.commit.sha : '', "new-merge-request-path" => namespace_project_new_merge_request_path, + "lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'), + "yml-help-page-path" => help_page_path('ci/yaml/README'), } } diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index 11adc7fd64a..69b20fbc6d0 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -6,34 +6,20 @@ - revert_commit = _('Revert this commit') - description = s_('ChangeTypeAction|This will create a new commit in order to revert the existing changes.') - title = commit.merged_merge_request(current_user) ? revert_merge_request : revert_commit + + - if defined?(pajamas) + .js-revert-commit-modal{ data: { title: title, + endpoint: revert_namespace_project_commit_path(commit, namespace_id: @project.namespace.full_path, project_id: @project), + branch: @project.default_branch, + push_code: can?(current_user, :push_code, @project).to_s, + branch_collaboration: @project.branch_allows_collaboration?(current_user, selected_branch).to_s, + existing_branch: ERB::Util.html_escape(selected_branch), + branches_endpoint: project_branches_path(@project) } } + - else + = render "projects/commit/commit_modal", title: title, type: type, commit: commit, branch_label: branch_label, description: description, label: label + - when 'cherry-pick' - label = s_('ChangeTypeAction|Cherry-pick') - branch_label = s_('ChangeTypeActionLabel|Pick into branch') - title = commit.merged_merge_request(current_user) ? _('Cherry-pick this merge request') : _('Cherry-pick this commit') - -.modal{ id: "modal-#{type}-commit", tabindex: -1 } - .modal-dialog - .modal-content - .modal-header - %h3.page-title= title - %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } - %span{ "aria-hidden": true } × - .modal-body - - if description - %p= description - = form_tag [type.underscore, @project, commit], method: :post, remote: false, class: "js-#{type}-form js-requires-input" do - .form-group.branch - = label_tag 'start_branch', branch_label, class: 'label-bold' - - = hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch' - = dropdown_tag(@project.default_branch, options: { title: s_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: s_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: project_branches_path(@project), submit_form_on_click: false } }) - - - if can?(current_user, :push_code, @project) - = render 'shared/new_merge_request_checkbox' - - else - = hidden_field_tag 'create_merge_request', 1, id: nil - .form-actions - = submit_tag label, class: 'gl-button btn btn-success' - = link_to _("Cancel"), '#', class: "gl-button btn btn-cancel", "data-dismiss" => "modal" - - = render 'shared/projects/edit_information' + = render "projects/commit/commit_modal", title: title, type: type, commit: commit, branch_label: branch_label, description: description, label: label diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 6f2797654d0..e8d524daced 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -37,7 +37,7 @@ #{ _('Browse Files') } - if can_collaborate && !@commit.has_been_reverted?(current_user) %li.clearfix - = revert_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false) + = revert_commit_link(@commit, project_commit_path(@project, @commit.id), pajamas: true) - if can_collaborate %li.clearfix = cherry_pick_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false) diff --git a/app/views/projects/commit/_commit_modal.html.haml b/app/views/projects/commit/_commit_modal.html.haml new file mode 100644 index 00000000000..a82d77fdc91 --- /dev/null +++ b/app/views/projects/commit/_commit_modal.html.haml @@ -0,0 +1,26 @@ +.modal{ id: "modal-#{type}-commit", tabindex: -1 } + .modal-dialog + .modal-content + .modal-header + %h3.page-title= title + %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } + %span{ "aria-hidden": true } × + .modal-body + - if description + %p= description + = form_tag [type.underscore, @project, commit], method: :post, remote: false, class: "js-#{type}-form js-requires-input" do + .form-group.branch + = label_tag 'start_branch', branch_label, class: 'label-bold' + + = hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch' + = dropdown_tag(@project.default_branch, options: { title: s_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: s_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: project_branches_path(@project), submit_form_on_click: false } }) + + - if can?(current_user, :push_code, @project) + = render 'shared/new_merge_request_checkbox' + - else + = hidden_field_tag 'create_merge_request', 1, id: nil + .form-actions + = submit_tag label, class: 'gl-button btn btn-success' + = link_to _("Cancel"), '#', class: "gl-button btn btn-cancel", "data-dismiss" => "modal" + + = render 'shared/projects/edit_information' diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 003a27f4c9a..e7b2e757ce4 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -17,5 +17,5 @@ .limited-width-notes = render "shared/notes/notes_with_form", :autocomplete => true - if can_collaborate_with_project?(@project) - - %w(revert cherry-pick).each do |type| - = render "projects/commit/change", type: type, commit: @commit, title: @commit.title + = render "projects/commit/change", type: 'revert', commit: @commit, pajamas: true + = render "projects/commit/change", type: 'cherry-pick', commit: @commit, title: @commit.title diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 387564f6408..cde8a5f69dd 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -3,17 +3,19 @@ - @content_class = "limit-container-width" unless fluid_layout - expanded = expanded_by_default? += render "shared/search_settings" + %section.settings.general-settings.no-animate.expanded#js-general-settings .settings-header %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Naming, topics, avatar') - %button.btn.btn-default.js-settings-toggle{ type: 'button' }= _('Collapse') + %button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }= _('Collapse') %p= _('Update your project name, topics, description, and avatar.') .settings-content= render 'projects/settings/general' %section.settings.sharing-permissions.no-animate#js-shared-permissions{ class: ('expanded' if expanded), data: { qa_selector: 'visibility_features_permissions_content' } } .settings-header %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Visibility, project features, permissions') - %button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand') + %button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand') %p= _('Choose visibility level, enable/disable project features (issues, repository, wiki, snippets) and set permissions.') .settings-content @@ -28,7 +30,7 @@ %section.qa-merge-request-settings.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] } .settings-header %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Merge requests') - %button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand') + %button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand') = render_if_exists 'projects/merge_request_settings_description_text' .settings-content @@ -46,11 +48,10 @@ .settings-header %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only = s_('ProjectSettings|Badges') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') + %button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand') %p - = s_('ProjectSettings|Customize your project badges.') - = link_to s_('ProjectSettings|Learn more about badges.'), help_page_path('user/project/badges') + = s_('ProjectSettings|Customize this project\'s badges.') + = link_to s_('ProjectSettings|What are badges?'), help_page_path('user/project/badges') .settings-content = render 'shared/badges/badge_settings' @@ -61,7 +62,7 @@ %section.qa-advanced-settings.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded) } .settings-header %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Advanced') - %button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand') + %button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand') %p= _('Housekeeping, export, path, transfer, remove, archive.') .settings-content diff --git a/app/views/projects/environments/_metrics_button.html.haml b/app/views/projects/environments/_metrics_button.html.haml index 5a691676a68..65abaf44082 100644 --- a/app/views/projects/environments/_metrics_button.html.haml +++ b/app/views/projects/environments/_metrics_button.html.haml @@ -3,5 +3,5 @@ - return unless can?(current_user, :read_environment, environment) = link_to environment_metrics_path(environment), title: _('See metrics'), class: 'gl-button btn metrics-button' do - = sprite_icon('chart') + = sprite_icon('chart', css_class: 'gl-mr-2') = _("Monitoring") diff --git a/app/views/projects/incidents/index.html.haml b/app/views/projects/incidents/index.html.haml index a89e93618bc..c29ab11a720 100644 --- a/app/views/projects/incidents/index.html.haml +++ b/app/views/projects/incidents/index.html.haml @@ -1,3 +1,4 @@ - page_title _('Incidents') +- add_page_specific_style 'page_bundles/incident_management_list' #js-incidents{ data: incidents_data(@project, params) } diff --git a/app/views/projects/issues/_design_management.html.haml b/app/views/projects/issues/_design_management.html.haml index ad0605b10a8..a2ff9620c0c 100644 --- a/app/views/projects/issues/_design_management.html.haml +++ b/app/views/projects/issues/_design_management.html.haml @@ -3,7 +3,7 @@ - requirements_link_url = help_page_path('user/project/issues/design_management', anchor: 'requirements') - requirements_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: requirements_link_url } - link_end = '</a>'.html_safe -- enable_lfs_message = s_("DesignManagement|To upload designs, you'll need to enable LFS and have admin enable hashed storage. %{requirements_link_start}More information%{requirements_link_end}").html_safe % { requirements_link_start: requirements_link_start, requirements_link_end: link_end } +- enable_lfs_message = s_("DesignManagement|To upload designs, you'll need to enable LFS and have an admin enable hashed storage. %{requirements_link_start}More information%{requirements_link_end}").html_safe % { requirements_link_start: requirements_link_start, requirements_link_end: link_end } - if @project.design_management_enabled? - add_page_startup_graphql_call('design_management/get_design_list', { fullPath: @project.full_path, iid: @issue.iid.to_s, atVersion: nil }) diff --git a/app/views/projects/issues/import_csv/_button.html.haml b/app/views/projects/issues/import_csv/_button.html.haml index ea8f53f7342..e60460687a9 100644 --- a/app/views/projects/issues/import_csv/_button.html.haml +++ b/app/views/projects/issues/import_csv/_button.html.haml @@ -2,7 +2,7 @@ - can_edit = can?(current_user, :admin_project, @project) .dropdown.btn-group - %button.btn.rounded-right.text-center{ class: ('has-tooltip' if type == :icon), title: (_('Import issues') if type == :icon), + %button.btn.gl-button.rounded-right.text-center{ class: ('has-tooltip' if type == :icon), title: (_('Import issues') if type == :icon), data: { toggle: 'dropdown', qa_selector: 'import_issues_button' }, 'aria-label' => _('Import issues'), 'aria-haspopup' => 'true', 'aria-expanded' => 'false' } - if type == :icon = sprite_icon('import') diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml index cd062fcf675..e14473708af 100644 --- a/app/views/projects/jobs/index.html.haml +++ b/app/views/projects/jobs/index.html.haml @@ -8,10 +8,11 @@ .nav-controls - if can?(current_user, :update_build, @project) - if !@repository.gitlab_ci_yml && !experiment_enabled?(:jobs_empty_state) - = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn gl-button btn-info js-empty-state-button' + = link_to s_('Pipelines|Get started with Pipelines'), help_page_path('ci/quick_start/README'), class: 'btn gl-button btn-info js-empty-state-button' = link_to project_ci_lint_path(@project), class: 'btn gl-button btn-default' do - %span CI lint + %span + = _('CI Lint') .content-list.builds-content-list = render "table", builds: @builds, project: @project diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index 1691a304e8b..6a42f33db7d 100644 --- a/app/views/projects/merge_requests/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml @@ -12,9 +12,9 @@ .detail-page-header.border-bottom-0.pt-0.pb-0 .detail-page-header-body - .issuable-status-box.status-box{ class: status_box_class(@merge_request) } - = sprite_icon(state_icon_name, css_class: 'd-block d-sm-none') - %span.d-none.d-sm-block + .issuable-status-box.status-box.js-mr-status-box{ class: status_box_class(@merge_request), data: { state: @merge_request.state } } + = sprite_icon(state_icon_name, css_class: 'gl-display-block gl-display-sm-none!') + %span.gl-display-none.gl-display-sm-block = state_human_name .issuable-meta diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index c70fc624dde..849cfac825f 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -57,6 +57,8 @@ = render "projects/merge_requests/awards_block" - if mr_action === "show" - add_page_startup_api_call discussions_path(@merge_request) + - add_page_startup_api_call widget_project_json_merge_request_path(@project, @merge_request, format: :json) + - add_page_startup_api_call cached_widget_project_json_merge_request_path(@project, @merge_request, format: :json) #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request).to_json, noteable_data: serialize_issuable(@merge_request, serializer: 'noteable'), noteable_type: 'MergeRequest', @@ -88,7 +90,8 @@ dismiss_endpoint: user_callouts_path, show_suggest_popover: show_suggest_popover?.to_s, show_whitespace_default: @show_whitespace_default.to_s, - file_by_file_default: @file_by_file_default.to_s } + file_by_file_default: @file_by_file_default.to_s, + default_suggestion_commit_message: default_suggestion_commit_message } .mr-loading-status .loading.hide diff --git a/app/views/projects/mirrors/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml index 97b04acea31..a91751da0aa 100644 --- a/app/views/projects/mirrors/_instructions.html.haml +++ b/app/views/projects/mirrors/_instructions.html.haml @@ -3,12 +3,12 @@ %li = html_escape(_('The repository must be accessible over %{code_open}http://%{code_close}, %{code_open}https://%{code_close}, %{code_open}ssh://%{code_close} or %{code_open}git://%{code_close}.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe } - %li= html_escape(_('When using the %{code_open}http://%{code_close} or %{code_open}https://%{code_close} protocols, please provide the exact URL to the repository. HTTP redirects will not be followed.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe } + %li= html_escape(_('When using the %{code_open}http://%{code_close} or %{code_open}https://%{code_close} protocols, provide the exact URL to the repository. HTTP redirects will not be followed.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe } %li= html_escape(_('Include the username in the URL if required: %{code_open}https://username@gitlab.company.com/group/project.git%{code_close}.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe } %li - minutes = Gitlab.config.gitlab_shell.git_timeout / 60 = _("The update action will time out after %{number_of_minutes} minutes. For big repositories, use a clone/push combination.") % { number_of_minutes: minutes } %li= mirror_lfs_sync_message %li - = _('This user will be the author of all events in the activity feed that are the result of an update, + = _('In case of pull mirroring, your user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches.') diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml index 5b074ff8a28..98d35845b31 100644 --- a/app/views/projects/mirrors/_mirror_repos.html.haml +++ b/app/views/projects/mirrors/_mirror_repos.html.haml @@ -10,7 +10,7 @@ = expanded ? _('Collapse') : _('Expand') %p = _('Set up your project to automatically push and/or pull changes to/from another repository. Branches, tags, and commits will be synced automatically.') - = link_to _('Read more'), help_page_path('user/project/repository/repository_mirroring.md'), target: '_blank' + = link_to _('How do I mirror repositories?'), help_page_path('user/project/repository/repository_mirroring'), target: '_blank', rel: 'noopener noreferrer' .settings-content - if mirror_settings_enabled @@ -21,7 +21,7 @@ .form-group.has-feedback = label_tag :url, _('Git repository URL'), class: 'label-light' - = text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url qa-mirror-repository-url-input', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+", autocomplete: 'new-password' + = text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url qa-mirror-repository-url-input', placeholder: _('Input the remote repository URL'), required: true, pattern: "(#{protocols}):\/\/.+", autocomplete: 'new-password' = render 'projects/mirrors/instructions' @@ -29,8 +29,10 @@ .form-check.gl-mb-3 = check_box_tag :only_protected_branches, '1', false, class: 'js-mirror-protected form-check-input' - = label_tag :only_protected_branches, _('Only mirror protected branches'), class: 'form-check-label' - = link_to sprite_icon('question-o'), help_page_path('user/project/protected_branches'), target: '_blank' + = label_tag :only_protected_branches, _('Mirror only protected branches'), class: 'form-check-label' + .form-text.text-muted + = _('If enabled, only protected branches will be mirrored.') + = link_to _('Learn more.'), help_page_path('user/project/repository/repository_mirroring', anchor: 'mirror-only-protected-branches'), target: '_blank', rel: 'noopener noreferrer' .panel-footer = f.submit _('Mirror repository'), class: 'gl-button btn btn-success js-mirror-submit qa-mirror-repository-button', name: :update_remote_mirror diff --git a/app/views/projects/mirrors/_mirror_repos_push.html.haml b/app/views/projects/mirrors/_mirror_repos_push.html.haml index 03839146f3b..04f44f4748e 100644 --- a/app/views/projects/mirrors/_mirror_repos_push.html.haml +++ b/app/views/projects/mirrors/_mirror_repos_push.html.haml @@ -10,4 +10,6 @@ .form-check.gl-mb-3 = check_box_tag :keep_divergent_refs, '1', false, class: 'js-mirror-keep-divergent-refs form-check-input' = label_tag :keep_divergent_refs, _('Keep divergent refs'), class: 'form-check-label' - = link_to sprite_icon('question-o'), help_page_path('user/project/repository/repository_mirroring', anchor: 'keep-divergent-refs'), target: '_blank' + .form-text.text-muted + = _('By default, if any ref (branch, tag, or commit) on the remote mirror has diverged from the local repository, the entire push will fail, and nothing will be updated. Choose this option to override this behavior. After the mirror is created, this can only be modified via the API.') + = link_to _('Learn more.'), help_page_path('user/project/repository/repository_mirroring', anchor: 'keep-divergent-refs'), target: '_blank', rel: 'noopener noreferrer' diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index a407aa9ac13..6af185696b0 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -8,7 +8,7 @@ .project-edit-errors = render 'projects/errors' - .js-experiment-new-project-creation{ data: { is_ci_cd_available: (ci_cd_projects_available? if Gitlab.ee?), has_errors: @project.errors.any? } } + .js-experiment-new-project-creation{ data: { is_ci_cd_available: (ci_cd_projects_available? if Gitlab.ee?), has_errors: @project.errors.any?, new_project_guidelines: brand_new_project_guidelines } } .row{ 'v-cloak': true } .col-lg-3.profile-settings-sidebar diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml index 2714b5f221a..99efb0b98c6 100644 --- a/app/views/projects/pages/_destroy.haml +++ b/app/views/projects/pages/_destroy.haml @@ -5,10 +5,10 @@ = s_('GitLabPages|Remove pages') .errors-holder .card-body - %p + %p.gl-mb-0 = s_('GitLabPages|Removing pages will prevent them from being exposed to the outside world.') - .form-actions - = link_to s_('GitLabPages|Remove pages'), project_pages_path(@project), data: { confirm: s_('GitLabPages|Are you sure?')}, method: :delete, class: "btn gl-button btn-danger" + .card-footer + = link_to s_('GitLabPages|Remove pages'), project_pages_path(@project), data: { confirm: s_('GitLabPages|Are you sure?')}, method: :delete, class: "btn gl-button btn-danger" - else .nothing-here-block = s_('GitLabPages|Only project maintainers can remove pages') diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml index ab44fd77e1e..e9ace8c72f1 100644 --- a/app/views/projects/pages/_use.html.haml +++ b/app/views/projects/pages/_use.html.haml @@ -3,7 +3,7 @@ .card-header.bg-info.text-white = s_('GitLabPages|Configure pages') .card-body - %p + %p.gl-mb-0 - link_start = "<a href='#{help_page_path('user/project/pages/index.md')}' target='_blank' rel='noopener noreferrer'>".html_safe - link_end = '</a>'.html_safe = s_('GitLabPages|Learn how to upload your static site and have it served by GitLab by following the %{link_start}documentation on GitLab Pages%{link_end}.').html_safe % { link_start: link_start, diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml index a52a6138402..90417a852d5 100644 --- a/app/views/projects/pipeline_schedules/index.html.haml +++ b/app/views/projects/pipeline_schedules/index.html.haml @@ -16,5 +16,5 @@ %ul.content-list = render partial: "table" - else - .card.bg-light + .card.bg-light.gl-mt-3 .nothing-here-block= _("No schedules") diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index f77f22cc555..77aa537dfdb 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -21,24 +21,24 @@ .icon-container = sprite_icon('flag') - if @pipeline.child? - %span.js-pipeline-child.badge.badge-primary.has-tooltip{ title: s_("Pipelines|This is a child pipeline within the parent pipeline") } + %span.js-pipeline-child.badge.badge-pill.gl-badge.sm.badge-primary.has-tooltip{ title: s_("Pipelines|This is a child pipeline within the parent pipeline") } = s_('Pipelines|Child pipeline') = surround '(', ')' do = link_to s_('Pipelines|parent'), pipeline_path(@pipeline.triggered_by_pipeline), class: 'text-white text-underline' - if @pipeline.latest? - %span.js-pipeline-url-latest.badge.badge-success.has-tooltip{ title: _("Latest pipeline for the most recent commit on this branch") } + %span.js-pipeline-url-latest.badge.badge-pill.gl-badge.sm.badge-success.has-tooltip{ title: _("Latest pipeline for the most recent commit on this branch") } latest - if @pipeline.has_yaml_errors? - %span.js-pipeline-url-yaml.badge.badge-danger.has-tooltip{ title: @pipeline.yaml_errors } + %span.js-pipeline-url-yaml.badge.badge-pill.gl-badge.sm.badge-danger.has-tooltip{ title: @pipeline.yaml_errors } yaml invalid - if @pipeline.failure_reason? - %span.js-pipeline-url-failure.badge.badge-danger.has-tooltip{ title: @pipeline.failure_reason } + %span.js-pipeline-url-failure.badge.badge-pill.gl-badge.sm.badge-danger.has-tooltip{ title: @pipeline.failure_reason } error - if @pipeline.auto_devops_source? - popover_title_text = html_escape(_('This pipeline makes use of a predefined CI/CD configuration enabled by %{b_open}Auto DevOps.%{b_close}')) % { b_open: '<b>'.html_safe, b_close: '</b>'.html_safe } - popover_content_url = help_page_path('topics/autodevops/index.md') - popover_content_text = _('Learn more about Auto DevOps') - %a.js-pipeline-url-autodevops.badge.badge-info.autodevops-badge{ href: "#", tabindex: "0", role: "button", data: { container: "body", + %a.js-pipeline-url-autodevops.badge.badge-pill.gl-badge.sm.badge-info.autodevops-badge{ href: "#", tabindex: "0", role: "button", data: { container: "body", toggle: "popover", placement: "top", html: "true", @@ -48,10 +48,10 @@ } } Auto DevOps - if @pipeline.detached_merge_request_pipeline? - %span.js-pipeline-url-mergerequest.badge.badge-info.has-tooltip{ title: _('Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for Pipelines for Merged Results.') } + %span.js-pipeline-url-mergerequest.badge.badge-pill.gl-badge.sm.badge-info.has-tooltip{ title: _('Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for Pipelines for Merged Results.') } detached - if @pipeline.stuck? - %span.js-pipeline-url-stuck.badge.badge-warning + %span.js-pipeline-url-stuck.badge.badge-pill.gl-badge.sm.badge-warning stuck .well-segment.branch-info diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml index f3360e150ad..ff728ab2fb3 100644 --- a/app/views/projects/pipelines/charts.html.haml +++ b/app/views/projects/pipelines/charts.html.haml @@ -1,10 +1,4 @@ - page_title _('CI / CD Analytics') -- if Feature.enabled?(:graphql_pipeline_analytics) - #js-project-pipelines-charts-app{ data: { project_path: @project.full_path } } -- else - #js-project-pipelines-charts-app{ data: { counts: @counts, success_ratio: success_ratio(@counts), - times_chart: { labels: @charts[:pipeline_times].labels, values: @charts[:pipeline_times].pipeline_times }, - last_week_chart: { labels: @charts[:week].labels, totals: @charts[:week].total, success: @charts[:week].success }, - last_month_chart: { labels: @charts[:month].labels, totals: @charts[:month].total, success: @charts[:month].success }, - last_year_chart: { labels: @charts[:year].labels, totals: @charts[:year].total, success: @charts[:year].success } } } +#js-project-pipelines-charts-app{ data: { project_path: @project.full_path, + should_render_deployment_frequency_charts: should_render_deployment_frequency_charts.to_s } } diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 64ae4ff8daf..6a4dd88ae07 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -17,4 +17,4 @@ "new-pipeline-path" => can?(current_user, :create_pipeline, @project) && new_project_pipeline_path(@project), "ci-lint-path" => can?(current_user, :create_pipeline, @project) && project_ci_lint_path(@project), "reset-cache-path" => can?(current_user, :admin_pipeline, @project) && reset_cache_project_settings_ci_cd_path(@project) , - "has-gitlab-ci" => (@project.has_ci? && @project.builds_enabled?).to_s } } + "has-gitlab-ci" => has_gitlab_ci?(@project).to_s } } diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index 847b96cbd0e..b3ad210aa47 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -6,6 +6,9 @@ - add_page_specific_style 'page_bundles/reports' - add_page_specific_style 'page_bundles/ci_status' +- if Feature.enabled?(:graphql_pipeline_details, @project) + - add_page_startup_graphql_call('pipelines/get_pipeline_details', { projectPath: @project.full_path, iid: @pipeline.iid }) + .js-pipeline-container{ data: { controller_action: "#{controller.action_name}" } } #js-pipeline-header-vue.pipeline-header-container{ data: { full_path: @project.full_path, pipeline_iid: @pipeline.iid, pipeline_id: @pipeline.id, pipelines_path: project_pipelines_path(@project) } } - if @pipeline.commit.present? diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml index 39ef1e52a0d..fe8a50ebb42 100644 --- a/app/views/projects/project_members/_groups.html.haml +++ b/app/views/projects/project_members/_groups.html.haml @@ -1,8 +1,11 @@ -.card.project-members-groups - .card-header - = html_escape(_("Groups with access to %{strong_open}%{project_name}%{strong_close}")) % { project_name: sanitize(@project.name, tags: []), strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe } - %span.badge.badge-pill= group_links.size - %ul.content-list.members-list - - can_admin_member = can?(current_user, :admin_project_member, @project) +.card.card-without-border + = render 'shared/members/tab_pane/header' do + = render 'shared/members/tab_pane/title' do + = html_escape(_("Groups with access to %{strong_open}%{project_name}%{strong_close}")) % { project_name: sanitize(@project.name, tags: []), strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe } + = form_tag project_project_members_path(@project), method: :get, class: 'user-search-form gl-mx-n3 gl-my-n3', data: { testid: 'group-link-search-form' } do + .gl-px-3.gl-py-2 + .search-control-wrap.gl-relative + = render 'shared/members/search_field', name: 'search_groups' + %ul.content-list.members-list{ data: { testid: 'project-member-groups' } } - @group_links.each do |group_link| - = render 'shared/members/group', group_link: group_link, can_admin_member: can_admin_member, group_link_path: project_group_link_path(@project, group_link) + = render 'shared/members/group', group_link: group_link, can_admin_member: can_manage_project_members?(@project), group_link_path: project_group_link_path(@project, group_link) diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index 171212b6a96..24ca7ebded9 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -1,20 +1,18 @@ - project = local_assigns.fetch(:project) - members = local_assigns.fetch(:members) - group = local_assigns.fetch(:group) -- current_user_is_group_owner = group && group.has_owner?(current_user) +- current_user_is_group_owner = local_assigns.fetch(:current_user_is_group_owner) -.card - .card-header.flex-project-members-panel - %span.flex-project-title +.card.card-without-border + = render 'shared/members/tab_pane/header' do + = render 'shared/members/tab_pane/title' do = html_escape(_("Members of %{strong_open}%{project_name}%{strong_close}")) % { project_name: sanitize(project.name, tags: []), strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe } - %span.badge.badge-pill= members.total_count - = form_tag project_project_members_path(project), method: :get, class: 'form-inline user-search-form flex-users-form' do - .form-group - .position-relative - = search_field_tag :search, params[:search], { placeholder: _('Find existing members by name'), class: 'form-control', spellcheck: false } - %button.user-search-btn{ type: "submit", "aria-label" => _("Submit search") } - = sprite_icon('search', css_class: 'gl-vertical-align-middle!') - = label_tag :sort_by, _('Sort by'), class: 'col-form-label label-bold px-2' + = form_tag project_project_members_path(project), method: :get, class: 'user-search-form gl-display-flex gl-md-align-items-center gl-flex-wrap gl-flex-direction-column gl-md-flex-direction-row gl-mx-n3 gl-my-n3', data: { testid: 'user-search-form' } do + .gl-px-3.gl-py-2 + .search-control-wrap.gl-relative + = render 'shared/members/search_field' + = render 'shared/members/tab_pane/form_item' do + = label_tag :sort_by, _('Sort by'), class: 'label-bold gl-mr-2 gl-mb-0 gl-py-2 align-self-md-center' = render 'shared/members/sort_dropdown' %ul.content-list.members-list{ data: { qa_selector: 'members_list', testid: 'members-table' } } = render partial: 'shared/members/member', diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index cad76d7aeac..cf39ac4dd56 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,44 +1,97 @@ - page_title _("Members") -- can_admin_project_members = can?(current_user, :admin_project_member, @project) - group = @project.group .js-remove-member-modal .row.gl-mt-3 .col-lg-12 - - if project_can_be_shared? - %h4 - = _("Project members") - - if can_admin_project_members - %p= share_project_description(@project) - - else - %p - = html_escape(_("Members can be added by project %{i_open}Maintainers%{i_close} or %{i_open}Owners%{i_close}")) % { i_open: '<i>'.html_safe, i_close: '</i>'.html_safe } + - if invite_members_allowed?(group) + .row + .col-md-12.col-lg-6.gl-display-flex + .gl-flex-direction-column.gl-flex-wrap.align-items-baseline + %h4 + = _("Project members") + .gl-justify-content-bottom.gl-display-flex.align-items-center + - if can_manage_project_members?(@project) + %p= share_project_description(@project) + - else + %p + = html_escape(_("Members can be added by project %{i_open}Maintainers%{i_close} or %{i_open}Owners%{i_close}")) % { i_open: '<i>'.html_safe, i_close: '</i>'.html_safe } + .col-md-12.col-lg-6 + .gl-display-flex.gl-flex-wrap.gl-lg-justify-content-end.gl-mx-n2.gl-mb-3 + .js-invite-members-trigger.gl-px-2.gl-sm-w-auto.gl-w-full.gl-mb-4{ data: { classes: 'btn btn-success gl-button gl-mt-3 gl-sm-w-auto gl-w-full', display_text: _('Invite members') } } + = render_if_exists 'projects/invite_members_modal', project: @project - .light - - if can_admin_project_members && project_can_be_shared? - - if !membership_locked? && @project.allowed_to_share_with_group? - %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' } - %li.nav-tab{ role: 'presentation' } - %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' }= _("Invite member") - %li.nav-tab{ role: 'presentation', class: ('active' if membership_locked?) } - %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab', qa_selector: 'invite_group_tab' }, role: 'tab' }= _("Invite group") + - else + - if project_can_be_shared? + %h4 + = _("Project members") + - if can_manage_project_members?(@project) + %p= share_project_description(@project) + - else + %p + = html_escape(_("Members can be added by project %{i_open}Maintainers%{i_close} or %{i_open}Owners%{i_close}")) % { i_open: '<i>'.html_safe, i_close: '</i>'.html_safe } - .tab-content.gitlab-tab-content - .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' } - = render 'shared/members/invite_member', submit_url: project_project_members_path(@project), access_levels: ProjectMember.access_level_roles, default_access_level: @project_member.access_level, can_import_members?: can_import_members?, import_path: import_project_project_members_path(@project) - .tab-pane{ id: 'invite-group-pane', role: 'tabpanel', class: ('active' if membership_locked?) } - = render 'shared/members/invite_group', submit_url: project_group_links_path(@project), access_levels: ProjectGroupLink.access_options, default_access_level: ProjectGroupLink.default_access, group_link_field: 'link_group_id', group_access_field: 'link_group_access' - - elsif !membership_locked? - .invite-member= render 'shared/members/invite_member', submit_url: project_project_members_path(@project), access_levels: ProjectMember.access_level_roles, default_access_level: @project_member.access_level, can_import_members?: can_import_members?, import_path: import_project_project_members_path(@project) - - elsif @project.allowed_to_share_with_group? - .invite-group= render 'shared/members/invite_group', access_levels: ProjectGroupLink.access_options, default_access_level: ProjectGroupLink.default_access, submit_url: project_group_links_path(@project), group_link_field: 'link_group_id', group_access_field: 'link_group_access' + - if !invite_members_allowed?(group) && can_manage_project_members?(@project) && project_can_be_shared? + - if !membership_locked? && @project.allowed_to_share_with_group? + %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' } + %li.nav-tab{ role: 'presentation' } + %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' }= _("Invite member") + %li.nav-tab{ role: 'presentation', class: ('active' if membership_locked?) } + %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab', qa_selector: 'invite_group_tab' }, role: 'tab' }= _("Invite group") - = render 'shared/members/requests', membership_source: @project, group: group, requesters: @requesters - .clearfix - %h5.member.existing-title - = _("Existing members and groups") - - if @group_links.any? + .tab-content.gitlab-tab-content + .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' } + = render 'shared/members/invite_member', submit_url: project_project_members_path(@project), access_levels: ProjectMember.access_level_roles, default_access_level: @project_member.access_level, can_import_members?: can_import_members?, import_path: import_project_project_members_path(@project) + .tab-pane{ id: 'invite-group-pane', role: 'tabpanel', class: ('active' if membership_locked?) } + = render 'shared/members/invite_group', submit_url: project_group_links_path(@project), access_levels: ProjectGroupLink.access_options, default_access_level: ProjectGroupLink.default_access, group_link_field: 'link_group_id', group_access_field: 'link_group_access' + - elsif !membership_locked? + .invite-member= render 'shared/members/invite_member', submit_url: project_project_members_path(@project), access_levels: ProjectMember.access_level_roles, default_access_level: @project_member.access_level, can_import_members?: can_import_members?, import_path: import_project_project_members_path(@project) + - elsif @project.allowed_to_share_with_group? + .invite-group= render 'shared/members/invite_group', access_levels: ProjectGroupLink.access_options, default_access_level: ProjectGroupLink.default_access, submit_url: project_group_links_path(@project), group_link_field: 'link_group_id', group_access_field: 'link_group_access' + %ul.nav-links.mobile-separator.nav.nav-tabs + %li.nav-item + = link_to '#tab-members', class: ['nav-link', ('active' unless groups_tab_active?)], data: { toggle: 'tab' } do + %span + = _('Members') + %span.badge.badge-pill= @project_members.total_count + - if show_groups?(@group_links) + %li.nav-item + = link_to '#tab-groups', class: ['nav-link', ('active' if groups_tab_active?)] , data: { toggle: 'tab', qa_selector: 'groups_list_tab' } do + %span + = _('Groups') + %span.badge.badge-pill= @group_links.count + - if show_invited_members?(@project, @invited_members) + %li.nav-item + = link_to '#tab-invited-members', class: 'nav-link', data: { toggle: 'tab' } do + %span + = _('Invited') + %span.badge.badge-pill= @invited_members.count + - if show_access_requests?(@project, @requesters) + %li.nav-item + = link_to '#tab-access-requests', class: 'nav-link', data: { toggle: 'tab' } do + %span + = _('Access requests') + %span.badge.badge-pill= @requesters.count + .tab-content + #tab-members.tab-pane{ class: ('active' unless groups_tab_active?) } + = render 'projects/project_members/team', project: @project, group: group, members: @project_members, current_user_is_group_owner: current_user_is_group_owner?(@project) + = paginate @project_members, theme: "gitlab", params: { search_groups: nil } + - if show_groups?(@group_links) + #tab-groups.tab-pane{ class: ('active' if groups_tab_active?) } = render 'projects/project_members/groups', group_links: @group_links - - = render 'projects/project_members/team', project: @project, group: group, members: @project_members - = paginate @project_members, theme: "gitlab" + - if show_invited_members?(@project, @invited_members) + #tab-invited-members.tab-pane + .card.card-without-border + = render 'shared/members/tab_pane/header' do + = render 'shared/members/tab_pane/title' do + = html_escape(_('Members invited to %{strong_start}%{project_name}%{strong_end}')) % { project_name: @project.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe } + %ul.content-list.members-list + = render partial: 'shared/members/member', collection: @invited_members, as: :member, locals: { membership_source: @project, group: group, current_user_is_group_owner: current_user_is_group_owner?(@project) } + - if show_access_requests?(@project, @requesters) + #tab-access-requests.tab-pane + .card.card-without-border + = render 'shared/members/tab_pane/header' do + = render 'shared/members/tab_pane/title' do + = html_escape(_('Users requesting access to %{strong_start}%{project_name}%{strong_end}')) % { project_name: @project.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe } + %ul.content-list.members-list + = render partial: 'shared/members/member', collection: @requesters, as: :member, locals: { membership_source: @project, group: group } diff --git a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml index 7131e9925b3..6ce01566a42 100644 --- a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml +++ b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml @@ -6,24 +6,21 @@ .card-body = form_errors(@protected_branch) .form-group.row - .col-md-2.text-right - = f.label :name, 'Branch:' + = f.label :name, s_('ProtectedBranch|Branch:'), class: 'col-md-2 text-left text-md-right' .col-md-10 = render partial: "projects/protected_branches/shared/dropdown", locals: { f: f } .form-text.text-muted - wildcards_url = help_page_url('user/project/protected_branches', anchor: 'wildcard-protected-branches') - wildcards_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: wildcards_url } - = (s_("ProtectedBranch|%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported") % { wildcards_link_start: wildcards_link_start, wildcards_link_end: '</a>', code_tag_start: '<code>', code_tag_end: '</code>' }).html_safe + = (s_("ProtectedBranch|%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported.") % { wildcards_link_start: wildcards_link_start, wildcards_link_end: '</a>', code_tag_start: '<code>', code_tag_end: '</code>' }).html_safe .form-group.row - %label.col-md-2.text-right{ for: 'merge_access_levels_attributes' } - = s_("ProtectedBranch|Allowed to merge:") + = f.label :merge_access_levels_attributes, s_("ProtectedBranch|Allowed to merge:"), class: 'col-md-2 text-left text-md-right' .col-md-10 = yield :merge_access_levels .form-group.row - %label.col-md-2.text-right{ for: 'push_access_levels_attributes' } - = s_("ProtectedBranch|Allowed to push:") + = f.label :push_access_levels_attributes, s_("ProtectedBranch|Allowed to push:"), class: 'col-md-2 text-left text-md-right' .col-md-10 = yield :push_access_levels - = render_if_exists 'projects/protected_branches/ee/code_owner_approval_form' + = render_if_exists 'projects/protected_branches/ee/code_owner_approval_form', f: f .card-footer = f.submit s_('ProtectedBranch|Protect'), class: 'btn-success btn', disabled: true, data: { qa_selector: 'protect_button' } diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml index f27936703de..66173c4759b 100644 --- a/app/views/projects/protected_branches/shared/_index.html.haml +++ b/app/views/projects/protected_branches/shared/_index.html.haml @@ -7,16 +7,15 @@ %button.btn.js-settings-toggle.qa-expand-protected-branches{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p - Keep stable branches secure and force developers to use merge requests. + Keep stable branches secure, and force developers to use merge requests. #{link_to "What are protected branches?", help_page_path("user/project/protected_branches")} .settings-content %p - By default, protected branches are designed to: + By default, protected branches protect your code and: %ul - %li prevent their creation, if not already created, from everybody except Maintainers - %li prevent pushes from everybody except Maintainers - %li prevent <strong>anyone</strong> from force pushing to the branch - %li prevent <strong>anyone</strong> from deleting the branch - %p Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches")} and #{link_to "project permissions", help_page_path("user/permissions")}. + %li Allow only users with Maintainer #{link_to "permissions", help_page_path("user/permissions")} to create new protected branches. + %li Allow only users with Maintainer permissions to push code. + %li Prevent <strong>anyone</strong> from force-pushing to the branch. + %li Prevent <strong>anyone</strong> from deleting the branch. - if can? current_user, :admin_project, @project = content_for :create_protected_branch diff --git a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml index c4bf2d20ecf..332cdd98e4a 100644 --- a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml +++ b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml @@ -2,24 +2,19 @@ %input{ type: 'hidden', name: 'update_section', value: 'js-protected-tags-settings' } .card .card-header - Protect a tag + = _('Protect a tag') .card-body = form_errors(@protected_tag) .form-group.row - .col-md-2.text-right - = f.label :name, 'Tag:' + = f.label :name, _('Tag:'), class: 'col-md-2 text-left text-md-right' .col-md-10.protected-tags-dropdown = render partial: "projects/protected_tags/shared/dropdown", locals: { f: f } .form-text.text-muted - = link_to 'Wildcards', help_page_path('user/project/protected_tags', anchor: 'wildcard-protected-tags') - such as - %code v* - or - %code *-release - are supported + - wildcards_url = help_page_path('user/project/protected_tags', anchor: 'wildcard-protected-tags') + - wildcards_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: wildcards_url } + = html_escape(_("%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}v*%{code_tag_end} or %{code_tag_start}*-release%{code_tag_end} are supported.")) % { wildcards_link_start: wildcards_link_start, wildcards_link_end: '</a>'.html_safe, code_tag_start: '<code>'.html_safe, code_tag_end: '</code>'.html_safe } .form-group.row - %label.col-md-2.text-right{ for: 'create_access_levels_attributes' } - Allowed to create: + = f.label :create_access_levels_attributes, _('Allowed to create:'), class: 'col-md-2 text-left text-md-right' .col-md-10 .create_access_levels-container = yield :create_access_levels diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml index 4bf3ce09fc7..5734b7dc3c9 100644 --- a/app/views/projects/protected_tags/shared/_index.html.haml +++ b/app/views/projects/protected_tags/shared/_index.html.haml @@ -7,16 +7,14 @@ %button.btn.js-settings-toggle{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p - Limit access to creating and updating tags. + Limit access to creating and updating tags. #{link_to "What are protected tags?", help_page_path("user/project/protected_tags")} .settings-content %p - By default, protected tags are designed to: + By default, protected tags protect your code and: %ul - %li Prevent tag creation by everybody except Maintainers - %li Prevent <strong>anyone</strong> from updating the tag - %li Prevent <strong>anyone</strong> from deleting the tag - - %p Read more about #{link_to "protected tags", help_page_path("user/project/protected_tags")}. + %li Allow only users with Maintainer #{link_to "permissions", help_page_path("user/permissions")} to create tags. + %li Prevent <strong>anyone</strong> from updating tags. + %li Prevent <strong>anyone</strong> from deleting tags. - if can? current_user, :admin_project, @project = yield :create_protected_tag diff --git a/app/views/projects/protected_tags/shared/_tags_list.html.haml b/app/views/projects/protected_tags/shared/_tags_list.html.haml index 382ea848243..a5a43072744 100644 --- a/app/views/projects/protected_tags/shared/_tags_list.html.haml +++ b/app/views/projects/protected_tags/shared/_tags_list.html.haml @@ -1,9 +1,9 @@ .protected-tags-list.js-protected-tags-list - if @protected_tags.empty? .card-header - Protected tag (#{@protected_tags_count}) + Protected tags (#{@protected_tags_count}) %p.settings-message.text-center - There are currently no protected tags, protect a tag with the form above. + No tags are protected. - else - can_admin_project = can?(current_user, :admin_project, @project) @@ -16,7 +16,7 @@ %col %thead %tr - %th Protected tag (#{@protected_tags_count}) + %th Protected tags (#{@protected_tags_count}) %th Last commit %th Allowed to create - if can_admin_project diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml index b53fbc97c02..97bc366544f 100644 --- a/app/views/projects/registry/repositories/index.html.haml +++ b/app/views/projects/registry/repositories/index.html.haml @@ -1,23 +1,22 @@ - page_title _("Container Registry") - @content_class = "limit-container-width" unless fluid_layout +- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @project.full_path, first: 10, name: nil, isGroupPage: false} ) %section - .row.registry-placeholder.prepend-bottom-10 - .col-12 - #js-container-registry{ data: { endpoint: project_container_registry_index_path(@project), - expiration_policy: @project.container_expiration_policy.to_json, - "help_page_path" => help_page_path('user/packages/container_registry/index'), - "two_factor_auth_help_link" => help_page_path('user/profile/account/two_factor_authentication'), - "personal_access_tokens_help_link" => help_page_path('user/profile/personal_access_tokens'), - "no_containers_image" => image_path('illustrations/docker-empty-state.svg'), - "containers_error_image" => image_path('illustrations/docker-error-state.svg'), - "repository_url" => escape_once(@project.container_registry_url), - "registry_host_url_with_port" => escape_once(registry_config.host_port), - "expiration_policy_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'expiration-policy'), - "garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'), - "run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'), - "cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'), - "project_path": @project.full_path, - "gid_prefix": container_repository_gid_prefix, - "is_admin": current_user&.admin.to_s, - character_error: @character_error.to_s } } + #js-container-registry{ data: { endpoint: project_container_registry_index_path(@project), + expiration_policy: @project.container_expiration_policy.to_json, + "help_page_path" => help_page_path('user/packages/container_registry/index'), + "two_factor_auth_help_link" => help_page_path('user/profile/account/two_factor_authentication'), + "personal_access_tokens_help_link" => help_page_path('user/profile/personal_access_tokens'), + "no_containers_image" => image_path('illustrations/docker-empty-state.svg'), + "containers_error_image" => image_path('illustrations/docker-error-state.svg'), + "repository_url" => escape_once(@project.container_registry_url), + "registry_host_url_with_port" => escape_once(registry_config.host_port), + "expiration_policy_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'expiration-policy'), + "garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'), + "run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'), + "cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'), + "project_path": @project.full_path, + "gid_prefix": container_repository_gid_prefix, + "is_admin": current_user&.admin.to_s, + character_error: @character_error.to_s } } diff --git a/app/views/projects/runners/_group_runners.html.haml b/app/views/projects/runners/_group_runners.html.haml index a24ada53bac..9415516d6f6 100644 --- a/app/views/projects/runners/_group_runners.html.haml +++ b/app/views/projects/runners/_group_runners.html.haml @@ -1,37 +1,40 @@ -- link = link_to _('Runners API'), help_page_path('api/runners.md') +- link = link_to _('Runner API'), help_page_path('api/runners.md') -%h3 - = _('Group Runners') +%h4 + = _('Group runners') .bs-callout.bs-callout-warning - = _('GitLab Group Runners can execute code for all the projects in this group.') - = _('They can be managed using the %{link}.').html_safe % { link: link } + = _('These runners are shared across projects in this group.') + %br + %br + = _('Group runners can be managed with the %{link}.').html_safe % { link: link } - if @project.group - %hr + %br + %br - if @project.group_runners_enabled? = link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-close', method: :post do - = _('Disable group Runners') + = _('Disable group runners') - else = link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-success btn-inverted', method: :post do - = _('Enable group Runners') + = _('Enable group runners') = _('for this project') - if !@project.group - = _('This project does not belong to a group and can therefore not make use of group Runners.') + = _('This project does not belong to a group and cannot make use of group runners.') - elsif @group_runners.empty? - = _('This group does not provide any group Runners yet.') + = _('This group does not have any group runners yet.') - if can?(current_user, :admin_pipeline, @project.group) - - group_link = link_to _('Group CI/CD settings'), group_settings_ci_cd_path(@project.group) + - group_link = link_to _("group's CI/CD settings."), group_settings_ci_cd_path(@project.group) = _('Group maintainers can register group runners in the %{link}').html_safe % { link: group_link } - else - = _('Ask your group maintainer to set up a group Runner.') + = _('Ask your group maintainer to set up a group runner.') - else %h4.underlined-title - = _('Available group Runners: %{runners}').html_safe % { runners: @group_runners.count } + = _('Available group runners: %{runners}').html_safe % { runners: @group_runners.count } %ul.bordered-list = render partial: 'projects/runners/runner', collection: @group_runners, as: :runner diff --git a/app/views/projects/runners/_index.html.haml b/app/views/projects/runners/_index.html.haml index ae4fee1e14c..a02bdac442b 100644 --- a/app/views/projects/runners/_index.html.haml +++ b/app/views/projects/runners/_index.html.haml @@ -1,8 +1,5 @@ = render 'shared/runners/runner_description' -%hr - -%p.lead= _('To start serving your jobs you can either add specific Runners to your project or use shared Runners') .row .col-sm-6 = render 'projects/runners/specific_runners' diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml index 1a3ba690184..85bd0335b92 100644 --- a/app/views/projects/runners/_runner.html.haml +++ b/app/views/projects/runners/_runner.html.haml @@ -23,7 +23,7 @@ - else = link_to _('Resume'), resume_project_runner_path(@project, runner), method: :post, class: 'btn btn-success btn-sm' - if runner.belongs_to_one_project? - = link_to _('Remove Runner'), project_runner_path(@project, runner), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm' + = link_to _('Remove runner'), project_runner_path(@project, runner), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm' - else - runner_project = @project.runner_projects.find_by(runner_id: runner) # rubocop: disable CodeReuse/ActiveRecord = link_to _('Disable for this project'), project_runner_project_path(@project, runner_project), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm' diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml index 4093f0a0719..fd8b4eb0d39 100644 --- a/app/views/projects/runners/_shared_runners.html.haml +++ b/app/views/projects/runners/_shared_runners.html.haml @@ -2,7 +2,8 @@ = render layout: 'shared/runners/shared_runners_description' do - if !isVueifySharedRunnersToggleEnabled - %hr + %br + %br - if @project.group&.shared_runners_setting == 'disabled_and_unoverridable' %h5.gl-text-red-500 = _('Shared runners disabled on group level') @@ -19,8 +20,8 @@ #toggle-shared-runners-form{ data: toggle_shared_runners_settings_data(@project) } - if @shared_runners_count == 0 - = _('This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area.') + = _('This GitLab instance does not provide any shared runners yet. Instance administrators can register shared runners in the admin area.') - else - %h4.underlined-title #{_('Available shared Runners:')} #{@shared_runners_count} + %h4.underlined-title #{_('Available shared runners:')} #{@shared_runners_count} %ul.bordered-list.available-shared-runners = render partial: 'projects/runners/runner', collection: @shared_runners, as: :runner diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index ed9e6aac346..3e325b80efd 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -1,7 +1,9 @@ -%h3 - = _('Specific Runners') +%h4 + = _('Specific runners') .bs-callout.help-callout + = _('These runners are specific to this project.') + %hr = render partial: 'ci/runner/how_to_setup_runner_automatically', locals: { type: 'specific', clusters_path: project_clusters_path(@project) } @@ -11,14 +13,16 @@ type: 'specific', reset_token_url: reset_registration_token_namespace_project_settings_ci_cd_path } +%hr + - if @project_runners.any? - %h4.underlined-title= _('Runners activated for this project') + %h4.underlined-title= _('Available specific runners') %ul.bordered-list.activated-specific-runners = render partial: 'projects/runners/runner', collection: @project_runners, as: :runner = paginate @project_runners, theme: "gitlab", param_name: "project_page", params: { expand_runners: true, anchor: 'js-runners-settings' } - if @assignable_runners.any? - %h4.underlined-title= _('Available specific runners') + %h4.underlined-title= _('Other available runners') %ul.bordered-list.available-specific-runners = render partial: 'projects/runners/runner', collection: @assignable_runners, as: :runner = paginate @assignable_runners, theme: "gitlab", param_name: "specific_page", :params => { :anchor => 'js-runners-settings'} diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml index b9d8e154913..f93cd23c83e 100644 --- a/app/views/projects/runners/edit.html.haml +++ b/app/views/projects/runners/edit.html.haml @@ -1,4 +1,4 @@ -- page_title _('Edit'), "#{@runner.description} ##{@runner.id}", _('Runners') +- page_title _('Edit'), "#{@runner.description} ##{@runner.id}", _('runners') %h4 Runner ##{@runner.id} diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 2b1e08f4880..59b3afa476f 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -5,10 +5,8 @@ .col-lg-4 %h3.page-title.gl-mt-0 = @service.title - - [true, false].each do |value| - - hide_class = 'd-none' if @service.operating? != value - %span.js-service-active-status{ class: hide_class, data: { value: value.to_s } } - = boolean_to_icon value + - if @service.operating? + = sprite_icon('check', css_class: 'gl-text-green-500') - if @service.respond_to?(:detailed_description) %p= @service.detailed_description diff --git a/app/views/projects/services/alerts/_help.html.haml b/app/views/projects/services/alerts/_help.html.haml deleted file mode 100644 index 7abd198bea5..00000000000 --- a/app/views/projects/services/alerts/_help.html.haml +++ /dev/null @@ -1 +0,0 @@ -.js-alerts-service-settings{ data: alerts_settings_data(disabled: true) } diff --git a/app/views/projects/services/alerts/_top.html.haml b/app/views/projects/services/alerts/_top.html.haml deleted file mode 100644 index e3bcb6bd3a0..00000000000 --- a/app/views/projects/services/alerts/_top.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -.row - .col-lg-12 - .gl-alert.gl-alert-info{ role: 'alert' } - = sprite_icon('information-o', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title') - .gl-alert-body - = _('You can now manage alert endpoint configuration in the Alerts section on the Operations settings page. Fields on this page have been deprecated.') - .gl-alert-actions - = link_to _('Visit settings page'), project_settings_operations_path(@project, anchor: 'js-alert-management-settings'), class: 'btn gl-alert-action btn-info new-gl-button' diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml index 453deff7756..2e4542a033e 100644 --- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml +++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml @@ -25,15 +25,15 @@ - if auto_devops_enabled %span.badge.badge-info.js-instance-default-badge= badge_for_auto_devops_scope(@project) .form-text.text-muted - = s_('CICD|The Auto DevOps pipeline will run if no alternative CI configuration file is found.') - = link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank' + = s_('CICD|The Auto DevOps pipeline runs if no alternative CI configuration file is found.') + = link_to _('Learn more.'), help_page_path('topics/autodevops/index.md'), target: '_blank' .card-footer.js-extra-settings{ class: auto_devops_enabled || 'hidden' } - if @project.all_clusters.empty? %p.settings-message.text-center - = s_('CICD|Add a %{kubernetes_cluster_link_start}Kubernetes cluster integration%{link_end} with a domain or create an AUTO_DEVOPS_PLATFORM_TARGET CI variable.').html_safe % { kubernetes_cluster_link_start: kubernetes_cluster_link_start, link_end: link_end } + = s_('CICD|Add a %{kubernetes_cluster_link_start}Kubernetes cluster integration%{link_end} with a domain, or create an AUTO_DEVOPS_PLATFORM_TARGET CI variable.').html_safe % { kubernetes_cluster_link_start: kubernetes_cluster_link_start, link_end: link_end } - elsif !has_base_domain %p.settings-message.text-center - = s_('CICD|You must add a %{base_domain_link_start}base domain%{link_end} to your %{kubernetes_cluster_link_start}Kubernetes cluster%{link_end} in order for your deployment strategy to work.').html_safe % { base_domain_link_start: base_domain_link_start, kubernetes_cluster_link_start: kubernetes_cluster_link_start, link_end: link_end } + = s_('CICD|Add a %{base_domain_link_start}base domain%{link_end} to your %{kubernetes_cluster_link_start}Kubernetes cluster%{link_end} for your deployment strategy to work.').html_safe % { base_domain_link_start: base_domain_link_start, kubernetes_cluster_link_start: kubernetes_cluster_link_start, link_end: link_end } %label.gl-mt-3 %strong= s_('CICD|Deployment strategy') .form-check diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index 0bef82ee325..55b6cf372fb 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -27,7 +27,7 @@ - quickstart_url = help_page_path('topics/autodevops/quick_start_guide') - auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url } - quickstart_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: quickstart_url } - = s_('AutoDevOps|Auto DevOps can automatically build, test, and deploy applications based on predefined continuous integration and delivery configuration. %{auto_devops_start}Learn more about Auto DevOps%{auto_devops_end} or use our %{quickstart_start}quick start guide%{quickstart_end} to get started right away.').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe, quickstart_start: quickstart_start, quickstart_end: '</a>'.html_safe } + = s_('AutoDevOps|%{auto_devops_start}Automate building, testing, and deploying%{auto_devops_end} your applications based on your continuous integration and delivery configuration. %{quickstart_start}How do I get started?%{quickstart_end}').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe, quickstart_start: quickstart_start, quickstart_end: '</a>'.html_safe } .settings-content = render 'autodevops_form', auto_devops_enabled: @project.auto_devops_enabled? @@ -40,11 +40,22 @@ %button.btn.js-settings-toggle{ type: 'button' } = expanded ? _('Collapse') : _('Expand') %p - = _("Runners are processes that pick up and execute jobs for GitLab. Here you can register and see your Runners for this project.") - = link_to s_('More information'), help_page_path('ci/runners/README') + = _("Runners are processes that pick up and execute CI/CD jobs for GitLab.") + = link_to s_('How do I configure runners?'), help_page_path('ci/runners/README') .settings-content = render 'projects/runners/index' +%section.settings.no-animate#js-artifacts-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _("Artifacts") + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') + %p + = _("A job artifact is an archive of files and directories saved by a job when it finishes.") + .settings-content + #js-artifacts-settings-app{ data: { full_path: @project.full_path, help_page_path: help_page_path('ci/pipelines/job_artifacts', anchor: 'keep-artifacts-from-most-recent-successful-jobs') } } + %section.qa-variables-settings.settings.no-animate#js-cicd-variables-settings{ class: ('expanded' if expanded), data: { qa_selector: 'variables_settings_content' } } .settings-header = render 'ci/variables/header', expanded: expanded @@ -71,7 +82,7 @@ = expanded ? _('Collapse') : _('Expand') %p = _("Save space and find images in the Container Registry. Remove unneeded tags and keep only the ones you want.") - = link_to _('More information'), help_page_path('user/packages/container_registry/index', anchor: 'cleanup-policy', target: '_blank', rel: 'noopener noreferrer') + = link_to _('How does cleanup work?'), help_page_path('user/packages/container_registry/index', anchor: 'cleanup-policy', target: '_blank', rel: 'noopener noreferrer') .settings-content = render 'projects/registry/settings/index' diff --git a/app/views/projects/settings/operations/_alert_management.html.haml b/app/views/projects/settings/operations/_alert_management.html.haml index 9e76ad52ecb..46f45df00df 100644 --- a/app/views/projects/settings/operations/_alert_management.html.haml +++ b/app/views/projects/settings/operations/_alert_management.html.haml @@ -1,6 +1,7 @@ - return unless can?(current_user, :admin_operations, @project) - expanded = expanded_by_default? - add_page_specific_style 'page_bundles/alert_management_settings' +- add_page_specific_style 'page_bundles/incident_management_list' %section.settings.no-animate#js-alert-management-settings{ class: ('expanded' if expanded) } .settings-header diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 5b9f868a71a..40faf91eadf 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -1,3 +1,5 @@ +- current_route_path = request.fullpath.match(/-\/tree\/[^\/]+\/(.+$)/).to_a[1] +- add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, path: current_route_path || "" }) - @content_class = "limit-container-width" unless fluid_layout - @skip_current_level_breadcrumb = true diff --git a/app/views/projects/snippets/verify.html.haml b/app/views/projects/snippets/verify.html.haml deleted file mode 100644 index 3c4f08e1df7..00000000000 --- a/app/views/projects/snippets/verify.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -= render 'layouts/recaptcha_verification', spammable: @snippet - diff --git a/app/views/projects/tracings/_tracing_button.html.haml b/app/views/projects/tracings/_tracing_button.html.haml index c9a6afd3761..b0ab6fa21e1 100644 --- a/app/views/projects/tracings/_tracing_button.html.haml +++ b/app/views/projects/tracings/_tracing_button.html.haml @@ -1,2 +1,2 @@ -= link_to project_settings_operations_path(@project), title: _('Configure Tracing'), class: 'btn btn-success' do += link_to project_settings_operations_path(@project), title: _('Configure Tracing'), class: 'gl-button btn btn-success' do = _('Add Jaeger URL') diff --git a/app/views/shared/_recaptcha_form.html.haml b/app/views/shared/_recaptcha_form.html.haml index 245a86721eb..aa9e9a34c90 100644 --- a/app/views/shared/_recaptcha_form.html.haml +++ b/app/views/shared/_recaptcha_form.html.haml @@ -9,8 +9,11 @@ - params[resource_name].each do |field, value| = hidden_field(resource_name, field, value: value) = hidden_field_tag(:spam_log_id, spammable.spam_log.id) - = hidden_field_tag(:recaptcha_verification, true) + -# The reCAPTCHA response value will be returned in the 'g-recaptcha-response' field = recaptcha_tags script: script, callback: 'recaptchaDialogCallback' unless Rails.env.test? + -# Fake the 'g-recaptcha-response' field in the test environment, so that the feature spec + -# can get to the (mocked) SpamVerdictService check. + = hidden_field_tag('g-recaptcha-response', 'abc123') if Rails.env.test? -# Yields a block with given extra params. = yield diff --git a/app/views/shared/_search_settings.html.haml b/app/views/shared/_search_settings.html.haml new file mode 100644 index 00000000000..ea3d7b97327 --- /dev/null +++ b/app/views/shared/_search_settings.html.haml @@ -0,0 +1,2 @@ +- if Feature.enabled?(:search_settings_in_page, @project, default_enabled: false) + .js-search-settings-app diff --git a/app/views/shared/_zen.html.haml b/app/views/shared/_zen.html.haml index 9cf189e8120..5a4efe7fe7f 100644 --- a/app/views/shared/_zen.html.haml +++ b/app/views/shared/_zen.html.haml @@ -3,6 +3,7 @@ - supports_autocomplete = local_assigns.fetch(:supports_autocomplete, true) - supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false) - qa_selector = local_assigns.fetch(:qa_selector, '') +- autofocus = local_assigns.fetch(:autofocus, false) .zen-backdrop - classes << ' js-gfm-input js-autosize markdown-area' - if defined?(f) && f @@ -12,7 +13,8 @@ dir: 'auto', data: { supports_quick_actions: supports_quick_actions, supports_autocomplete: supports_autocomplete, - qa_selector: qa_selector } + qa_selector: qa_selector, + autofocus: autofocus } - else = text_area_tag attr, current_text, data: { qa_selector: qa_selector }, class: classes, placeholder: placeholder %a.zen-control.zen-control-leave.js-zen-leave.gl-text-gray-500{ href: "#" } diff --git a/app/views/shared/deploy_tokens/_index.html.haml b/app/views/shared/deploy_tokens/_index.html.haml index 540b9b0054f..c26400690a6 100644 --- a/app/views/shared/deploy_tokens/_index.html.haml +++ b/app/views/shared/deploy_tokens/_index.html.haml @@ -3,7 +3,7 @@ %section.qa-deploy-tokens-settings.settings.no-animate#js-deploy-tokens{ class: ('expanded' if expanded), data: { qa_selector: 'deploy_tokens_settings_content' } } .settings-header %h4= s_('DeployTokens|Deploy Tokens') - %button.btn.js-settings-toggle.qa-expand-deploy-keys{ type: 'button' } + %button.gl-button.btn.js-settings-toggle.qa-expand-deploy-keys{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p = description diff --git a/app/views/shared/deploy_tokens/_revoke_modal.html.haml b/app/views/shared/deploy_tokens/_revoke_modal.html.haml index 5a3759ef755..2b31c675f74 100644 --- a/app/views/shared/deploy_tokens/_revoke_modal.html.haml +++ b/app/views/shared/deploy_tokens/_revoke_modal.html.haml @@ -10,6 +10,6 @@ %p = s_('DeployTokens|You are about to revoke %{b_start}%{name}%{b_end}.').html_safe % { b_start: '<b>'.html_safe, name: token.name, b_end: '</b>'.html_safe } = s_('DeployTokens|This action cannot be undone.') - .modal-footer - %a{ href: '#', data: { dismiss: 'modal' }, class: 'btn btn-default' }= _('Cancel') - = link_to s_('DeployTokens|Revoke %{name}') % { name: token.name }, revoke_deploy_token_path(group_or_project, token), method: :put, class: 'btn btn-danger' + .modal-footer.gl-flex-direction-row + %a{ href: '#', data: { dismiss: 'modal' }, class: 'gl-button btn btn-default' }= _('Cancel') + = link_to s_('DeployTokens|Revoke %{name}') % { name: token.name }, revoke_deploy_token_path(group_or_project, token), method: :put, class: 'gl-button btn btn-danger text-truncate' diff --git a/app/views/shared/empty_states/_snippets.html.haml b/app/views/shared/empty_states/_snippets.html.haml index db8da50d868..aa762782c46 100644 --- a/app/views/shared/empty_states/_snippets.html.haml +++ b/app/views/shared/empty_states/_snippets.html.haml @@ -1,6 +1,6 @@ - button_path = local_assigns.fetch(:button_path, false) -.row.empty-state.mt-0 +.row.empty-state .col-12 .svg-content = image_tag 'illustrations/snippets_empty.svg', data: { qa_selector: 'svg_content' } @@ -16,5 +16,3 @@ = link_to s_('SnippetsEmptyState|Documentation'), help_page_path('user/snippets.md'), class: 'btn btn-default', title: s_('SnippetsEmptyState|Documentation') - else %h4.text-center= s_('SnippetsEmptyState|There are no snippets to show.') - - diff --git a/app/views/shared/empty_states/_wikis.html.haml b/app/views/shared/empty_states/_wikis.html.haml index 656acafd416..4150406a4ea 100644 --- a/app/views/shared/empty_states/_wikis.html.haml +++ b/app/views/shared/empty_states/_wikis.html.haml @@ -3,7 +3,7 @@ - if can?(current_user, :create_wiki, @wiki.container) - create_path = wiki_page_path(@wiki, params[:id], view: 'create') - - create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn btn-success qa-create-first-page-link', title: s_('WikiEmpty|Create your first page') + - create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn gl-button btn-success qa-create-first-page-link', title: s_('WikiEmpty|Create your first page') = render layout: layout_path, locals: { image_path: 'illustrations/wiki_login_empty.svg' } do %h4.text-left @@ -14,11 +14,11 @@ - if show_enable_confluence_integration?(@wiki.container) = link_to s_('WikiEmpty|Enable the Confluence Wiki integration'), edit_project_service_path(@project, :confluence), - class: 'btn', title: s_('WikiEmpty|Enable the Confluence Wiki integration') + class: 'btn gl-button', title: s_('WikiEmpty|Enable the Confluence Wiki integration') - elsif @project && can?(current_user, :read_issue, @project) - issues_link = link_to s_('WikiEmptyIssueMessage|issue tracker'), project_issues_path(@project) - - new_issue_link = link_to s_('WikiEmpty|Suggest wiki improvement'), new_project_issue_path(@project), class: 'btn btn-success', title: s_('WikiEmptyIssueMessage|Suggest wiki improvement') + - new_issue_link = link_to s_('WikiEmpty|Suggest wiki improvement'), new_project_issue_path(@project), class: 'btn gl-button btn-success', title: s_('WikiEmptyIssueMessage|Suggest wiki improvement') = render layout: layout_path, locals: { image_path: 'illustrations/wiki_logout_empty.svg' } do %h4 diff --git a/app/views/shared/integrations/_index.html.haml b/app/views/shared/integrations/_index.html.haml index edc85f04d91..ccc2c448f69 100644 --- a/app/views/shared/integrations/_index.html.haml +++ b/app/views/shared/integrations/_index.html.haml @@ -16,7 +16,8 @@ - activated_label = (integration.activated? ? s_("ProjectService|%{service_title}: status on") : s_("ProjectService|%{service_title}: status off")) % { service_title: integration.title } %tr{ role: 'row' } %td{ role: 'cell', 'aria-colindex': 1, 'aria-label': activated_label, title: activated_label } - = boolean_to_icon integration.operating? + - if integration.operating? + = sprite_icon('check', css_class: 'gl-text-green-500') %td{ role: 'cell', 'aria-colindex': 2 } = link_to integration.title, scoped_edit_integration_path(integration), class: 'gl-font-weight-bold', data: { qa_selector: "#{integration.to_param}_link" } %td.d-none.d-sm-table-cell{ role: 'cell', 'aria-colindex': 3 } diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index cd265c10451..911bef482dd 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -2,6 +2,7 @@ This should be removed when this sidebar is converted to Vue since assignee data is also available in the `issuable_sidebar` hash - issuable_type = issuable_sidebar[:type] +- show_forwarding_email = !issuable_sidebar[:create_note_email].nil? - signed_in = !!issuable_sidebar.dig(:current_user, :id) - can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit) - add_page_startup_api_call "#{issuable_sidebar[:issuable_json_path]}?serializer=sidebar_extras" @@ -57,7 +58,7 @@ .selectbox.hide-collapsed = f.hidden_field 'milestone_id', value: milestone[:id], id: nil = dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable_type}[milestone_id]", project_id: issuable_sidebar[:project_id], issuable_id: issuable_sidebar[:id], ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], use_id: true, default_no: true, selected: milestone[:title], null_default: true, display: 'static' }}) - - if @project.group.present? + - if @project.group.present? && issuable_sidebar[:supports_iterations] = render_if_exists 'shared/issuable/iteration_select', can_edit: can_edit_issuable, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type - if issuable_sidebar[:supports_time_tracking] @@ -65,7 +66,7 @@ // Fallback while content is loading .title.hide-collapsed = _('Time tracking') - = loading_icon + = loading_icon(css_class: 'gl-vertical-align-text-bottom') - if issuable_sidebar.has_key?(:due_date) .block.due_date .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable_sidebar[:due_date]) } @@ -145,12 +146,15 @@ = _('Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}').html_safe % { source_branch_open: "<cite class='ref-name' title='#{source_branch}'>".html_safe, source_branch_close: "</cite>".html_safe, source_branch: source_branch } = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport') + - if show_forwarding_email + .block + #issuable-copy-email - if issuable_sidebar.dig(:current_user, :can_move) .block.js-sidebar-move-issue-block .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') } = custom_icon('icon_arrow_right') .dropdown.sidebar-move-issue-dropdown.hide-collapsed - %button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button', + %button.gl-button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button', data: { toggle: 'dropdown', display: 'static', track_label: "right_sidebar", track_property: "move_issue", track_event: "click_button", track_value: "" } } = _('Move issue') .dropdown-menu.dropdown-menu-selectable.dropdown-extended-height @@ -159,7 +163,7 @@ = dropdown_content = dropdown_loading = dropdown_footer add_content_class: true do - %button.btn.btn-success.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ type: 'button', disabled: true } + %button.gl-button.btn.btn-success.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ type: 'button', disabled: true } = _('Move') = loading_icon(css_class: 'gl-vertical-align-text-bottom sidebar-move-issue-confirmation-loading-icon') diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml index a425f5f810e..70e931ac164 100644 --- a/app/views/shared/issuable/form/_branch_chooser.html.haml +++ b/app/views/shared/issuable/form/_branch_chooser.html.haml @@ -6,6 +6,17 @@ - source_title, target_title = format_mr_branch_names(@merge_request) +- vis010 = _('This merge request is from a private project to an internal project.') +- vis020 = _('This merge request is from a private project to a public project.') +- vis1020 = _('This merge request is from an internal project to a public project.') +- i18n = { '010' => vis010, '020' => vis020, '1020' => vis1020 } + +- source_level = @merge_request.source_project.visibility_level +- source_visibility = @merge_request.source_project.visibility +- target_level = @merge_request.target_project.visibility_level + +- visibilityMismatchString = i18n["#{source_level}#{target_level}"] + .form-group.row.d-flex.gl-px-5.branch-selector .align-self-center %span @@ -24,4 +35,12 @@ = form.hidden_field(:target_branch, { class: 'target_branch js-target-branch-select ref-name mw-xl', data: { placeholder: _('Select branch'), endpoint: refs_project_path(@project, sort: 'updated_desc', find: 'branches') }}) + +- if source_level < target_level + .gl-alert.gl-alert-warning.gl-mt-4 + = sprite_icon('warning', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title') + .gl-alert-body + = visibilityMismatchString + %br + = _('Review the target project before submitting to avoid exposing %{source} changes.') % { source: source_visibility } %hr diff --git a/app/views/shared/members/_badge.html.haml b/app/views/shared/members/_badge.html.haml index e304207f3e9..5f925ff0cad 100644 --- a/app/views/shared/members/_badge.html.haml +++ b/app/views/shared/members/_badge.html.haml @@ -1,4 +1,4 @@ - type ||= 'info' %span.px-1.py-1 - %span{ class: "badge badge-#{type}" }= yield + %span{ class: "badge badge-pill gl-badge sm badge-#{type}" }= yield diff --git a/app/views/shared/members/_invite_member.html.haml b/app/views/shared/members/_invite_member.html.haml index 59b0600e2dd..0302b2fc3cf 100644 --- a/app/views/shared/members/_invite_member.html.haml +++ b/app/views/shared/members/_invite_member.html.haml @@ -23,6 +23,6 @@ .clearable-input = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' = sprite_icon('close', size: 16, css_class: 'clear-icon js-clear-input gl-text-gray-200') - = submit_tag _("Invite"), class: "btn btn-success", data: { qa_selector: 'invite_member_button' } + = submit_tag _("Invite"), class: "gl-button btn btn-success", data: { qa_selector: 'invite_member_button' } - if can_import_members - = link_to _("Import"), import_path, class: "btn btn-default", title: _("Import members from another project") + = link_to _("Import"), import_path, class: "gl-button btn btn-default", title: _("Import members from another project") diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 79bbb74d601..c76051a25b2 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -127,8 +127,5 @@ = _("Delete") - unless force_mobile_view = sprite_icon('remove', css_class: 'd-none d-sm-block gl-icon') - = render_if_exists 'shared/members/ee/override_member_buttons', group: group, member: member, user: user, action: :edit, can_override: member.can_override? - else %span.member-access-text.user-access-role= member.human_access - -= render_if_exists 'shared/members/ee/override_member_buttons', group: group, member: member, user: user, action: :confirm, can_override: member.can_override? diff --git a/app/views/groups/group_members/tab_pane/_form_item.html.haml b/app/views/shared/members/tab_pane/_form_item.html.haml index 9e57d3329d7..9e57d3329d7 100644 --- a/app/views/groups/group_members/tab_pane/_form_item.html.haml +++ b/app/views/shared/members/tab_pane/_form_item.html.haml diff --git a/app/views/groups/group_members/tab_pane/_header.html.haml b/app/views/shared/members/tab_pane/_header.html.haml index a02bf90eddf..a02bf90eddf 100644 --- a/app/views/groups/group_members/tab_pane/_header.html.haml +++ b/app/views/shared/members/tab_pane/_header.html.haml diff --git a/app/views/groups/group_members/tab_pane/_title.html.haml b/app/views/shared/members/tab_pane/_title.html.haml index c1418a5f7c8..c1418a5f7c8 100644 --- a/app/views/groups/group_members/tab_pane/_title.html.haml +++ b/app/views/shared/members/tab_pane/_title.html.haml diff --git a/app/views/shared/milestones/_header.html.haml b/app/views/shared/milestones/_header.html.haml index 19ca00ce482..541d7a52385 100644 --- a/app/views/shared/milestones/_header.html.haml +++ b/app/views/shared/milestones/_header.html.haml @@ -14,12 +14,9 @@ = link_to _('Edit'), edit_milestone_path(milestone), class: 'btn gl-button btn-grouped' - if milestone.project_milestone? && milestone.project.group - %button.js-promote-project-milestone-button.btn.gl-button.btn-grouped{ data: { toggle: 'modal', - target: '#promote-milestone-modal', - milestone_title: milestone.title, + %button.js-promote-project-milestone-button.btn.gl-button.btn-grouped{ data: { milestone_title: milestone.title, group_name: milestone.project.group.name, - url: promote_project_milestone_path(milestone.project, milestone), - container: 'body' }, + url: promote_project_milestone_path(milestone.project, milestone)}, disabled: true, type: 'button' } = _('Promote') diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index 92ac6929e6a..48a97ed66bb 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -51,10 +51,7 @@ type: 'button', data: { url: promote_project_milestone_path(milestone.project, milestone), milestone_title: milestone.title, - group_name: @project.group.name, - target: '#promote-milestone-modal', - container: 'body', - toggle: 'modal' } } + group_name: @project.group.name } } = sprite_icon('level-up', size: 14) - if can?(current_user, :admin_milestone, milestone) diff --git a/app/views/shared/promotions/_promote_servicedesk.html.haml b/app/views/shared/promotions/_promote_servicedesk.html.haml index 237416a869b..3a5123acdeb 100644 --- a/app/views/shared/promotions/_promote_servicedesk.html.haml +++ b/app/views/shared/promotions/_promote_servicedesk.html.haml @@ -6,8 +6,6 @@ = custom_icon('icon_service_desk') .user-callout-copy %h4 - = _("Improve customer support with GitLab Service Desk.") + = _("Improve customer support with Service Desk") %p - = _("GitLab Service Desk is a simple way to allow people to create issues in your GitLab instance without needing their own user account. It provides a unique email address for end users to create issues in a project, and replies can be sent either through the GitLab interface or by email. End users will only see the thread through email.") - = link_to _('Read more'), help_page_path('user/project/service_desk.md'), target: '_blank' - + = _("Service Desk allows people to create issues in your GitLab instance without their own user account. It provides a unique email address for end users to create issues in a project. Replies can be sent either through the GitLab interface or by email. End users only see threads through email.") diff --git a/app/views/shared/runners/_form.html.haml b/app/views/shared/runners/_form.html.haml index 675a8f922c4..bb2aa93740e 100644 --- a/app/views/shared/runners/_form.html.haml +++ b/app/views/shared/runners/_form.html.haml @@ -5,7 +5,7 @@ .col-sm-10 .form-check = f.check_box :active, { class: 'form-check-input' } - %label.light{ for: :runner_active }= _("Paused Runners don't accept new jobs") + %label.light{ for: :runner_active }= _("Paused runners don't accept new jobs") .form-group.row = label :protected, _("Protected"), class: 'col-form-label col-sm-2' .col-sm-10 @@ -40,13 +40,13 @@ = _('Maximum job timeout') .col-sm-10 = f.text_field :maximum_timeout_human_readable, class: 'form-control' - .form-text.text-muted= _('This timeout will take precedence when lower than project-defined timeout and accepts a human readable time input language like "1 hour". Values without specification represent seconds.') + .form-text.text-muted= _('Enter the number of seconds, or other human-readable input, like "1 hour". This timeout takes precedence over lower timeouts set for the project.') .form-group.row = label_tag :tag_list, class: 'col-form-label col-sm-2' do = _('Tags') .col-sm-10 = f.text_field :tag_list, value: runner.tag_list.sort.join(', '), class: 'form-control' - .form-text.text-muted= _('You can set up jobs to only use Runners with specific tags. Separate tags with commas.') + .form-text.text-muted= _('You can set up jobs to only use runners with specific tags. Separate tags with commas.') - if local_assigns[:in_gitlab_com_admin_context] .form-group.row = label_tag :public_projects_minutes_cost_factor, class: 'col-form-label col-sm-2' do diff --git a/app/views/shared/runners/_runner_description.html.haml b/app/views/shared/runners/_runner_description.html.haml index d3e50cfe92f..6a65145d42b 100644 --- a/app/views/shared/runners/_runner_description.html.haml +++ b/app/views/shared/runners/_runner_description.html.haml @@ -1,16 +1,12 @@ .light.gl-mt-3 %p - = _("You can set up as many Runners as you need to run your jobs.") - %br - = _('Runners can be placed on separate users, servers, and even on your local machine.') + = _("Register as many runners as you want. You can register runners as separate users, on separate servers, and on your local machine. Runners are either:") - %p - = _('Each Runner can be in one of the following states:') %div %ul %li - %span.badge.badge-success active - = _('- Runner is active and can process any new jobs') + %span.badge.badge-pill.gl-badge.sm.badge-success active + = _('- Available to run jobs.') %li - %span.badge.badge-danger paused - = _('- Runner is paused and will not receive any new jobs') + %span.badge.badge-pill.gl-badge.sm.badge-danger paused + = _('- Not available to run jobs.') diff --git a/app/views/shared/runners/_shared_runners_description.html.haml b/app/views/shared/runners/_shared_runners_description.html.haml index b9fb518b1aa..92564ec48bd 100644 --- a/app/views/shared/runners/_shared_runners_description.html.haml +++ b/app/views/shared/runners/_shared_runners_description.html.haml @@ -1,9 +1,11 @@ - link = link_to _('MaxBuilds'), 'https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnersmachine-section', target: '_blank' -%h3 +%h4 = _('Shared runners') .bs-callout.shared-runners-description + = _('These runners are shared across this GitLab instance.') + %p - if Gitlab::CurrentSettings.shared_runners_text.present? = markdown_field(Gitlab::CurrentSettings.current_application_settings, :shared_runners_text) - else diff --git a/app/views/shared/web_hooks/_hook.html.haml b/app/views/shared/web_hooks/_hook.html.haml index a957f9f6dfa..13fe8f76bd3 100644 --- a/app/views/shared/web_hooks/_hook.html.haml +++ b/app/views/shared/web_hooks/_hook.html.haml @@ -5,8 +5,8 @@ %div - hook.class.triggers.each_value do |trigger| - if hook.public_send(trigger) - %span.badge.badge-gray.deploy-project-label= trigger.to_s.titleize - %span.badge.badge-gray + %span.gl-badge.gl-bg-gray-10.gl-mt-2.rounded.deploy-project-label= trigger.to_s.titleize + %span.gl-badge.gl-bg-gray-10.gl-mt-2.rounded = _('SSL Verification:') = hook.enable_ssl_verification ? _('enabled') : _('disabled') diff --git a/app/views/shared/wikis/_form.html.haml b/app/views/shared/wikis/_form.html.haml index b6504c7a17e..5fd22665633 100644 --- a/app/views/shared/wikis/_form.html.haml +++ b/app/views/shared/wikis/_form.html.haml @@ -42,7 +42,7 @@ .col-sm-2.col-form-label= f.label :content, class: 'control-label-full-width' .col-sm-10 = render layout: 'shared/md_preview', locals: { url: wiki_page_path(@wiki, @page, action: :preview_markdown) } do - = render 'shared/zen', f: f, attr: :content, classes: 'note-textarea qa-wiki-content-textarea', placeholder: s_("WikiPage|Write your content or drag files hereā¦") + = render 'shared/zen', f: f, attr: :content, classes: 'note-textarea qa-wiki-content-textarea', placeholder: s_("WikiPage|Write your content or drag files hereā¦"), autofocus: @page.persisted? = render 'shared/notes/hints' .clearfix @@ -70,10 +70,10 @@ .form-actions - if @page && @page.persisted? - = f.submit _("Save changes"), class: 'btn-success btn qa-save-changes-button' + = f.submit _("Save changes"), class: 'btn gl-button btn-success qa-save-changes-button js-wiki-btn-submit', disabled: 'true' .float-right - = link_to _("Cancel"), wiki_page_path(@wiki, @page), class: 'btn gl-button btn-cancel btn-grouped' + = link_to _("Cancel"), wiki_page_path(@wiki, @page), class: 'btn gl-button btn-cancel btn-default' - else - = f.submit s_("Wiki|Create page"), class: 'btn-success btn qa-create-page-button rspec-create-page-button' + = f.submit s_("Wiki|Create page"), class: 'btn-success gl-button btn qa-create-page-button rspec-create-page-button js-wiki-btn-submit', disabled: 'true' .float-right - = link_to _("Cancel"), wiki_path(@wiki), class: 'btn gl-button btn-cancel' + = link_to _("Cancel"), wiki_path(@wiki), class: 'btn gl-button btn-cancel btn-default' diff --git a/app/views/shared/wikis/_sidebar.html.haml b/app/views/shared/wikis/_sidebar.html.haml index a906bf7aa63..4e9fdc8b95a 100644 --- a/app/views/shared/wikis/_sidebar.html.haml +++ b/app/views/shared/wikis/_sidebar.html.haml @@ -1,13 +1,23 @@ +- editing ||= false + %aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "50", "spy" => "affix" } } .sidebar-container .block.wiki-sidebar-header.gl-mb-3.w-100 %a.gutter-toggle.float-right.d-block.d-md-none.js-sidebar-wiki-toggle{ href: "#" } = sprite_icon('chevron-double-lg-right', css_class: 'gl-icon') - - git_access_url = wiki_path(@wiki, action: :git_access) - = link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '', data: { qa_selector: 'clone_repository_link' } do - = sprite_icon('download', css_class: 'gl-mr-2') - %span= _("Clone repository") + .gl-display-flex.gl-flex-wrap + - git_access_url = wiki_path(@wiki, action: :git_access) + = link_to git_access_url, class: 'gl-mr-5' + (active_nav_link?(path: 'wikis#git_access') ? ' active' : ''), data: { qa_selector: 'clone_repository_link' } do + = sprite_icon('download', css_class: 'gl-mr-2') + %span= _("Clone repository") + + - if can?(current_user, :create_wiki, @wiki) + - edit_sidebar_url = wiki_page_path(@wiki, Wiki::SIDEBAR, action: :edit) + - link_class = (editing && @page&.slug == Wiki::SIDEBAR) ? 'active' : '' + = link_to edit_sidebar_url, class: link_class, data: { qa_selector: 'edit_sidebar_link' } do + = sprite_icon('pencil-square', css_class: 'gl-mr-2') + %span= _("Edit sidebar") - if @sidebar_error.present? = render 'shared/alert_info', body: s_('Wiki|The sidebar failed to load. You can reload the page to try again.') diff --git a/app/views/shared/wikis/diff.html.haml b/app/views/shared/wikis/diff.html.haml index 68bbbd66f4a..19167f04855 100644 --- a/app/views/shared/wikis/diff.html.haml +++ b/app/views/shared/wikis/diff.html.haml @@ -5,12 +5,11 @@ .wiki-page-header.top-area.has-sidebar-toggle.flex-column.flex-lg-row = wiki_sidebar_toggle_button - .nav-text - %h2.wiki-page-title - = link_to_wiki_page @page - %span.light - · - = _('Changes') + %h3.page-title.gl-flex-fill-1 + = link_to_wiki_page @page + %span.light + · + = _('Changes') .nav-controls.pb-md-3.pb-lg-0 = link_to wiki_page_path(@wiki, @page, action: :history), class: 'btn gl-button', role: 'button', data: { qa_selector: 'page_history_button' } do diff --git a/app/views/shared/wikis/edit.html.haml b/app/views/shared/wikis/edit.html.haml index 834749caaba..c2b0e474c03 100644 --- a/app/views/shared/wikis/edit.html.haml +++ b/app/views/shared/wikis/edit.html.haml @@ -6,15 +6,14 @@ .wiki-page-header.top-area.has-sidebar-toggle.flex-column.flex-lg-row = wiki_sidebar_toggle_button - .nav-text - %h2.wiki-page-title - - if @page.persisted? - = link_to_wiki_page @page - %span.light - · - = s_("Wiki|Edit Page") - - else - = s_("Wiki|Create New Page") + %h3.page-title.gl-flex-fill-1 + - if @page.persisted? + = link_to_wiki_page @page + %span.light + · + = s_("Wiki|Edit Page") + - else + = s_("Wiki|Create New Page") .nav-controls.pb-md-3.pb-lg-0 - if @page.persisted? @@ -23,4 +22,4 @@ = render 'shared/wikis/form', uploads_path: wiki_attachment_upload_url -= render 'shared/wikis/sidebar' += render 'shared/wikis/sidebar', editing: true diff --git a/app/views/shared/wikis/history.html.haml b/app/views/shared/wikis/history.html.haml index 50ccfdeabd5..b1dcd2cd400 100644 --- a/app/views/shared/wikis/history.html.haml +++ b/app/views/shared/wikis/history.html.haml @@ -4,12 +4,11 @@ .wiki-page-header.top-area.has-sidebar-toggle.flex-column.flex-lg-row = wiki_sidebar_toggle_button - .nav-text - %h2.wiki-page-title - = link_to_wiki_page @page - %span.light - · - = _('History') + %h3.page-title + = link_to_wiki_page @page + %span.light + · + = _('History') .prepend-top-default.gl-mb-3 .table-holder diff --git a/app/views/shared/wikis/pages.html.haml b/app/views/shared/wikis/pages.html.haml index 76fc9510740..f5ba1c83de4 100644 --- a/app/views/shared/wikis/pages.html.haml +++ b/app/views/shared/wikis/pages.html.haml @@ -6,9 +6,9 @@ .wiki-page-header.top-area.flex-column.flex-lg-row - .nav-text.flex-fill - %h2.wiki-page-title - = s_("Wiki|Wiki Pages") + + %h3.page-title.gl-flex-fill-1 + = s_("Wiki|Wiki Pages") .nav-controls.pb-md-3.pb-lg-0 = link_to wiki_path(@wiki, action: :git_access), class: 'btn gl-button' do diff --git a/app/views/shared/wikis/show.html.haml b/app/views/shared/wikis/show.html.haml index 6f1c1a3a801..6d14ba8fe7b 100644 --- a/app/views/shared/wikis/show.html.haml +++ b/app/views/shared/wikis/show.html.haml @@ -7,7 +7,7 @@ .nav-text.flex-fill %span.wiki-last-edit-by - if @page.last_version - = (_("Last edited by %{name}") % { name: "<strong>#{@page.last_version.author_name}</strong>" }).html_safe + = html_escape(_("Last edited by %{link_start}%{avatar} %{name}%{link_end}")) % { avatar: image_tag(avatar_icon_for_email(@page.last_version.author_email, 24), class: "avatar s24 float-none gl-mr-0!"), name: "<strong>#{@page.last_version.author_name}</strong>".html_safe, link_start: "<a href='#{@page.last_version.author_url}'>".html_safe, link_end: '</a>'.html_safe } = time_ago_with_tooltip(@page.last_version.authored_date) .nav-controls.pb-md-3.pb-lg-0 diff --git a/app/views/sherlock/transactions/show.html.haml b/app/views/sherlock/transactions/show.html.haml index 8f2b36123bb..162b14f01e1 100644 --- a/app/views/sherlock/transactions/show.html.haml +++ b/app/views/sherlock/transactions/show.html.haml @@ -18,8 +18,8 @@ .row-content-block .float-right - = link_to(sherlock_transactions_path, class: 'btn') do - = sprite_icon('arrow-left') + = link_to(sherlock_transactions_path, class: 'gl-button btn') do + = sprite_icon('arrow-left', css_class: 'gl-mr-3') = t('sherlock.all_transactions') .oneline = t('sherlock.transaction') diff --git a/app/views/snippets/verify.html.haml b/app/views/snippets/verify.html.haml deleted file mode 100644 index 3c4f08e1df7..00000000000 --- a/app/views/snippets/verify.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -= render 'layouts/recaptcha_verification', spammable: @snippet - diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 1f2e8213b64..4c4a314a1e6 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -147,6 +147,14 @@ :weight: 1 :idempotent: :tags: [] +- :name: cronjob:ci_pipeline_artifacts_expire_artifacts + :feature_category: :continuous_integration + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: cronjob:ci_platform_metrics_update_cron :feature_category: :continuous_integration :has_external_dependencies: @@ -188,7 +196,7 @@ :idempotent: :tags: [] - :name: cronjob:gitlab_usage_ping - :feature_category: :collection + :feature_category: :usage_ping :has_external_dependencies: :urgency: :low :resource_boundary: :unknown @@ -881,7 +889,7 @@ :urgency: :low :resource_boundary: :unknown :weight: 1 - :idempotent: + :idempotent: true :tags: [] - :name: jira_connect:jira_connect_sync_builds :feature_category: :integrations @@ -891,13 +899,29 @@ :weight: 1 :idempotent: true :tags: [] +- :name: jira_connect:jira_connect_sync_deployments + :feature_category: :integrations + :has_external_dependencies: true + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] +- :name: jira_connect:jira_connect_sync_feature_flags + :feature_category: :integrations + :has_external_dependencies: true + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: jira_connect:jira_connect_sync_merge_request :feature_category: :integrations :has_external_dependencies: true :urgency: :low :resource_boundary: :unknown :weight: 1 - :idempotent: + :idempotent: true :tags: [] - :name: jira_connect:jira_connect_sync_project :feature_category: :integrations @@ -1070,22 +1094,22 @@ :idempotent: true :tags: [] - :name: pipeline_background:ci_daily_build_group_report_results - :feature_category: :continuous_integration + :feature_category: :code_testing :has_external_dependencies: :urgency: :low :resource_boundary: :unknown :weight: 1 :idempotent: true :tags: [] -- :name: pipeline_background:ci_pipeline_success_unlock_artifacts - :feature_category: :continuous_integration +- :name: pipeline_background:ci_pipeline_artifacts_coverage_report + :feature_category: :code_testing :has_external_dependencies: :urgency: :low :resource_boundary: :unknown :weight: 1 :idempotent: true :tags: [] -- :name: pipeline_background:ci_pipelines_create_artifact +- :name: pipeline_background:ci_pipeline_success_unlock_artifacts :feature_category: :continuous_integration :has_external_dependencies: :urgency: :low @@ -1418,6 +1442,14 @@ :tags: [] - :name: bulk_import :feature_category: :importers + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: + :tags: [] +- :name: bulk_imports_entity + :feature_category: :importers :has_external_dependencies: true :urgency: :low :resource_boundary: :unknown @@ -1577,6 +1609,14 @@ :weight: 1 :idempotent: :tags: [] +- :name: experiments_record_conversion_event + :feature_category: :users + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: expire_build_instance_artifacts :feature_category: :continuous_integration :has_external_dependencies: @@ -1787,6 +1827,14 @@ :weight: 1 :idempotent: :tags: [] +- :name: namespaces_onboarding_pipeline_created + :feature_category: :subgroups + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: namespaces_onboarding_user_added :feature_category: :users :has_external_dependencies: @@ -2095,6 +2143,22 @@ :weight: 1 :idempotent: :tags: [] +- :name: snippet_schedule_bulk_repository_shard_moves + :feature_category: :gitaly + :has_external_dependencies: + :urgency: :throttled + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] +- :name: snippet_update_repository_storage + :feature_category: :gitaly + :has_external_dependencies: + :urgency: :throttled + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: system_hook_push :feature_category: :source_code_management :has_external_dependencies: diff --git a/app/workers/bulk_import_worker.rb b/app/workers/bulk_import_worker.rb index 7828d046036..81099d4e5f7 100644 --- a/app/workers/bulk_import_worker.rb +++ b/app/workers/bulk_import_worker.rb @@ -7,9 +7,58 @@ class BulkImportWorker # rubocop:disable Scalability/IdempotentWorker sidekiq_options retry: false, dead: false - worker_has_external_dependencies! + PERFORM_DELAY = 5.seconds + DEFAULT_BATCH_SIZE = 5 def perform(bulk_import_id) - BulkImports::Importers::GroupsImporter.new(bulk_import_id).execute + @bulk_import = BulkImport.find_by_id(bulk_import_id) + + return unless @bulk_import + return if @bulk_import.finished? + return @bulk_import.finish! if all_entities_processed? && @bulk_import.started? + return re_enqueue if max_batch_size_exceeded? # Do not start more jobs if max allowed are already running + + @bulk_import.start! if @bulk_import.created? + + created_entities.first(next_batch_size).each do |entity| + entity.start! + + BulkImports::EntityWorker.perform_async(entity.id) + end + + re_enqueue + end + + private + + def entities + @entities ||= @bulk_import.entities + end + + def started_entities + entities.with_status(:started) + end + + def created_entities + entities.with_status(:created) + end + + def all_entities_processed? + entities.all? { |entity| entity.finished? || entity.failed? } + end + + def max_batch_size_exceeded? + started_entities.count >= DEFAULT_BATCH_SIZE + end + + def next_batch_size + [DEFAULT_BATCH_SIZE - started_entities.count, 0].max + end + + # A new BulkImportWorker job is enqueued to either + # - Process the new BulkImports::Entity created during import (e.g. for the subgroups) + # - Or to mark the `bulk_import` as finished + def re_enqueue + BulkImportWorker.perform_in(PERFORM_DELAY, @bulk_import.id) end end diff --git a/app/workers/bulk_imports/entity_worker.rb b/app/workers/bulk_imports/entity_worker.rb new file mode 100644 index 00000000000..9b29ad8f326 --- /dev/null +++ b/app/workers/bulk_imports/entity_worker.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module BulkImports + class EntityWorker # rubocop:disable Scalability/IdempotentWorker + include ApplicationWorker + + feature_category :importers + + sidekiq_options retry: false, dead: false + + worker_has_external_dependencies! + + def perform(entity_id) + entity = BulkImports::Entity.with_status(:started).find_by_id(entity_id) + + if entity + entity.update!(jid: jid) + + BulkImports::Importers::GroupImporter.new(entity).execute + end + end + end +end diff --git a/app/workers/ci/daily_build_group_report_results_worker.rb b/app/workers/ci/daily_build_group_report_results_worker.rb index a6d3c485e24..687cadc6366 100644 --- a/app/workers/ci/daily_build_group_report_results_worker.rb +++ b/app/workers/ci/daily_build_group_report_results_worker.rb @@ -5,6 +5,8 @@ module Ci include ApplicationWorker include PipelineBackgroundQueue + feature_category :code_testing + idempotent! def perform(pipeline_id) diff --git a/app/workers/ci/pipelines/create_artifact_worker.rb b/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb index 220df975503..4de56f54f44 100644 --- a/app/workers/ci/pipelines/create_artifact_worker.rb +++ b/app/workers/ci/pipeline_artifacts/coverage_report_worker.rb @@ -1,16 +1,18 @@ # frozen_string_literal: true module Ci - module Pipelines - class CreateArtifactWorker + module PipelineArtifacts + class CoverageReportWorker include ApplicationWorker include PipelineBackgroundQueue + feature_category :code_testing + idempotent! def perform(pipeline_id) Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline| - Ci::Pipelines::CreateArtifactService.new.execute(pipeline) + Ci::PipelineArtifacts::CoverageReportService.new.execute(pipeline) end end end diff --git a/app/workers/ci/pipeline_artifacts/expire_artifacts_worker.rb b/app/workers/ci/pipeline_artifacts/expire_artifacts_worker.rb new file mode 100644 index 00000000000..0bb911bc6c8 --- /dev/null +++ b/app/workers/ci/pipeline_artifacts/expire_artifacts_worker.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Ci + module PipelineArtifacts + class ExpireArtifactsWorker + include ApplicationWorker + # rubocop:disable Scalability/CronWorkerContext + # This worker does not perform work scoped to a context + include CronjobQueue + # rubocop:enable Scalability/CronWorkerContext + + deduplicate :until_executed, including_scheduled: true + idempotent! + feature_category :continuous_integration + + def perform + service = ::Ci::PipelineArtifacts::DestroyExpiredArtifactsService.new + artifacts_count = service.execute + log_extra_metadata_on_done(:destroyed_pipeline_artifacts_count, artifacts_count) + end + end + end +end diff --git a/app/workers/concerns/update_repository_storage_worker.rb b/app/workers/concerns/update_repository_storage_worker.rb new file mode 100644 index 00000000000..f46b64895a2 --- /dev/null +++ b/app/workers/concerns/update_repository_storage_worker.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module UpdateRepositoryStorageWorker + extend ActiveSupport::Concern + include ApplicationWorker + + included do + idempotent! + feature_category :gitaly + urgency :throttled + end + + def perform(container_id, new_repository_storage_key, repository_storage_move_id = nil) + repository_storage_move = + if repository_storage_move_id + find_repository_storage_move(repository_storage_move_id) + else + # maintain compatibility with workers queued before release + container = find_container(container_id) + container.repository_storage_moves.create!( + source_storage_name: container.repository_storage, + destination_storage_name: new_repository_storage_key + ) + end + + update_repository_storage(repository_storage_move) + end + + private + + def find_repository_storage_move(repository_storage_move_id) + raise NotImplementedError + end + + def find_container(container_id) + raise NotImplementedError + end + + def update_repository_storage(repository_storage_move) + raise NotImplementedError + end +end diff --git a/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb b/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb index 8c3c2e9e103..7c86b194574 100644 --- a/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb +++ b/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb @@ -12,6 +12,14 @@ module ContainerExpirationPolicies worker_resource_boundary :unknown idempotent! + LOG_ON_DONE_FIELDS = %i[ + cleanup_status + cleanup_tags_service_original_size + cleanup_tags_service_before_truncate_size + cleanup_tags_service_after_truncate_size + cleanup_tags_service_before_delete_size + ].freeze + def perform_work return unless throttling_enabled? return unless container_repository @@ -26,7 +34,7 @@ module ContainerExpirationPolicies result = ContainerExpirationPolicies::CleanupService.new(container_repository) .execute - log_extra_metadata_on_done(:cleanup_status, result.payload[:cleanup_status]) + log_on_done(result) end def remaining_work_count @@ -92,5 +100,22 @@ module ContainerExpirationPolicies def log_info(extra_structure) logger.info(structured_payload(extra_structure)) end + + def log_on_done(result) + LOG_ON_DONE_FIELDS.each do |field| + value = result.payload[field] + + next if value.nil? + + log_extra_metadata_on_done(field, value) + end + + before_truncate_size = result.payload[:cleanup_tags_service_before_truncate_size] + after_truncate_size = result.payload[:cleanup_tags_service_after_truncate_size] + truncated = before_truncate_size && + after_truncate_size && + before_truncate_size != after_truncate_size + log_extra_metadata_on_done(:cleanup_tags_service_truncated, !!truncated) + end end end diff --git a/app/workers/experiments/record_conversion_event_worker.rb b/app/workers/experiments/record_conversion_event_worker.rb new file mode 100644 index 00000000000..e38ce7b3d01 --- /dev/null +++ b/app/workers/experiments/record_conversion_event_worker.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Experiments + class RecordConversionEventWorker + include ApplicationWorker + + feature_category :users + urgency :low + + idempotent! + + def perform(experiment, user_id) + return unless Gitlab::Experimentation.active?(experiment) + + ::Experiment.record_conversion_event(experiment, user_id) + end + end +end diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb index 12372961250..5db9f0b67e0 100644 --- a/app/workers/expire_build_artifacts_worker.rb +++ b/app/workers/expire_build_artifacts_worker.rb @@ -10,6 +10,8 @@ class ExpireBuildArtifactsWorker # rubocop:disable Scalability/IdempotentWorker feature_category :continuous_integration def perform - Ci::DestroyExpiredJobArtifactsService.new.execute + service = Ci::DestroyExpiredJobArtifactsService.new + artifacts_count = service.execute + log_extra_metadata_on_done(:destroyed_job_artifacts_count, artifacts_count) end end diff --git a/app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb b/app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb index 0809d0b7c29..790e8b0eccf 100644 --- a/app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb +++ b/app/workers/gitlab/github_import/stage/import_pull_requests_reviews_worker.rb @@ -11,18 +11,11 @@ module Gitlab # client - An instance of Gitlab::GithubImport::Client. # project - An instance of Project. def import(client, project) - waiter = - if Feature.enabled?(:github_import_pull_request_reviews, project, default_enabled: true) - waiter = Importer::PullRequestsReviewsImporter - .new(project, client) - .execute + waiter = Importer::PullRequestsReviewsImporter + .new(project, client) + .execute - project.import_state.refresh_jid_expiration - - waiter - else - JobWaiter.new - end + project.import_state.refresh_jid_expiration AdvanceStageWorker.perform_async( project.id, diff --git a/app/workers/gitlab_performance_bar_stats_worker.rb b/app/workers/gitlab_performance_bar_stats_worker.rb index d63f8111864..558df0ab7b3 100644 --- a/app/workers/gitlab_performance_bar_stats_worker.rb +++ b/app/workers/gitlab_performance_bar_stats_worker.rb @@ -7,12 +7,13 @@ class GitlabPerformanceBarStatsWorker LEASE_TIMEOUT = 600 WORKER_DELAY = 120 STATS_KEY = 'performance_bar_stats:pending_request_ids' + STATS_KEY_EXPIRE = 30.minutes.to_i feature_category :metrics idempotent! def perform(lease_uuid) - Gitlab::Redis::SharedState.with do |redis| + Gitlab::Redis::Cache.with do |redis| request_ids = fetch_request_ids(redis, lease_uuid) stats = Gitlab::PerformanceBar::Stats.new(redis) diff --git a/app/workers/gitlab_usage_ping_worker.rb b/app/workers/gitlab_usage_ping_worker.rb index 1bb600bbd13..782b089261f 100644 --- a/app/workers/gitlab_usage_ping_worker.rb +++ b/app/workers/gitlab_usage_ping_worker.rb @@ -8,7 +8,7 @@ class GitlabUsagePingWorker # rubocop:disable Scalability/IdempotentWorker include CronjobQueue # rubocop:disable Scalability/CronWorkerContext include Gitlab::ExclusiveLeaseHelpers - feature_category :collection + feature_category :usage_ping sidekiq_options retry: 3, dead: false sidekiq_retry_in { |count| (count + 1) * 8.hours.to_i } diff --git a/app/workers/issuable_export_csv_worker.rb b/app/workers/issuable_export_csv_worker.rb index d91ba77287f..33452b14edb 100644 --- a/app/workers/issuable_export_csv_worker.rb +++ b/app/workers/issuable_export_csv_worker.rb @@ -7,47 +7,45 @@ class IssuableExportCsvWorker # rubocop:disable Scalability/IdempotentWorker worker_resource_boundary :cpu loggable_arguments 2 - PERMITTED_TYPES = [:merge_request, :issue].freeze - def perform(type, current_user_id, project_id, params) - @type = type.to_sym - check_permitted_type! - process_params!(params, project_id) - - @current_user = User.find(current_user_id) - @project = Project.find(project_id) - @service = service(find_objects(params)) + user = User.find(current_user_id) + project = Project.find(project_id) + finder_params = map_params(params, project_id) - @service.email(@current_user) + export_service(type.to_sym, user, project, finder_params).email(user) + rescue ActiveRecord::RecordNotFound => error + logger.error("Failed to export CSV (current_user_id:#{current_user_id}, project_id:#{project_id}): #{error.message}") end private - def find_objects(params) - case @type - when :issue - IssuesFinder.new(@current_user, params).execute - when :merge_request - MergeRequestsFinder.new(@current_user, params).execute - end + def map_params(params, project_id) + params + .symbolize_keys + .except(:sort) + .merge(project_id: project_id) end - def service(issuables) - case @type + def export_service(type, user, project, params) + issuable_class = service_classes_for(type) + issuables = issuable_class[:finder].new(user, params).execute + issuable_class[:service].new(issuables, project) + end + + def service_classes_for(type) + case type when :issue - Issues::ExportCsvService.new(issuables, @project) + { finder: IssuesFinder, service: Issues::ExportCsvService } when :merge_request - MergeRequests::ExportCsvService.new(issuables, @project) + { finder: MergeRequestsFinder, service: MergeRequests::ExportCsvService } + else + raise ArgumentError, type_error_message(type) end end - def process_params!(params, project_id) - params.symbolize_keys! - params[:project_id] = project_id - params.delete(:sort) - end - - def check_permitted_type! - raise ArgumentError, "type parameter must be :issue or :merge_request, it was #{@type}" unless PERMITTED_TYPES.include?(@type) + def type_error_message(type) + "Type parameter must be :issue or :merge_request, it was #{type}" end end + +IssuableExportCsvWorker.prepend_if_ee('::EE::IssuableExportCsvWorker') diff --git a/app/workers/jira_connect/sync_branch_worker.rb b/app/workers/jira_connect/sync_branch_worker.rb index d7e773b0861..1af51c4bb74 100644 --- a/app/workers/jira_connect/sync_branch_worker.rb +++ b/app/workers/jira_connect/sync_branch_worker.rb @@ -8,8 +8,9 @@ module JiraConnect feature_category :integrations loggable_arguments 1, 2 worker_has_external_dependencies! + idempotent! - def perform(project_id, branch_name, commit_shas, update_sequence_id = nil) + def perform(project_id, branch_name, commit_shas, update_sequence_id) project = Project.find_by_id(project_id) return unless project diff --git a/app/workers/jira_connect/sync_deployments_worker.rb b/app/workers/jira_connect/sync_deployments_worker.rb new file mode 100644 index 00000000000..0f261e29464 --- /dev/null +++ b/app/workers/jira_connect/sync_deployments_worker.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module JiraConnect + class SyncDeploymentsWorker + include ApplicationWorker + + idempotent! + worker_has_external_dependencies! + + queue_namespace :jira_connect + feature_category :integrations + + def perform(deployment_id, sequence_id) + deployment = Deployment.find_by_id(deployment_id) + + return unless deployment + return unless Feature.enabled?(:jira_sync_deployments, deployment.project) + + ::JiraConnect::SyncService + .new(deployment.project) + .execute(deployments: [deployment], update_sequence_id: sequence_id) + end + + def self.perform_async(id) + seq_id = ::Atlassian::JiraConnect::Client.generate_update_sequence_id + super(id, seq_id) + end + end +end diff --git a/app/workers/jira_connect/sync_feature_flags_worker.rb b/app/workers/jira_connect/sync_feature_flags_worker.rb new file mode 100644 index 00000000000..7e98d0eada7 --- /dev/null +++ b/app/workers/jira_connect/sync_feature_flags_worker.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module JiraConnect + class SyncFeatureFlagsWorker + include ApplicationWorker + + idempotent! + worker_has_external_dependencies! + + queue_namespace :jira_connect + feature_category :integrations + + def perform(feature_flag_id, sequence_id) + feature_flag = ::Operations::FeatureFlag.find_by_id(feature_flag_id) + + return unless feature_flag + return unless Feature.enabled?(:jira_sync_feature_flags, feature_flag.project) + + ::JiraConnect::SyncService + .new(feature_flag.project) + .execute(feature_flags: [feature_flag], update_sequence_id: sequence_id) + end + end +end diff --git a/app/workers/jira_connect/sync_merge_request_worker.rb b/app/workers/jira_connect/sync_merge_request_worker.rb index 6ef426790b3..543d8e002fe 100644 --- a/app/workers/jira_connect/sync_merge_request_worker.rb +++ b/app/workers/jira_connect/sync_merge_request_worker.rb @@ -6,10 +6,11 @@ module JiraConnect queue_namespace :jira_connect feature_category :integrations + idempotent! worker_has_external_dependencies! - def perform(merge_request_id, update_sequence_id = nil) + def perform(merge_request_id, update_sequence_id) merge_request = MergeRequest.find_by_id(merge_request_id) return unless merge_request && merge_request.project diff --git a/app/workers/namespaces/onboarding_pipeline_created_worker.rb b/app/workers/namespaces/onboarding_pipeline_created_worker.rb new file mode 100644 index 00000000000..e1de6d0046b --- /dev/null +++ b/app/workers/namespaces/onboarding_pipeline_created_worker.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Namespaces + class OnboardingPipelineCreatedWorker + include ApplicationWorker + + feature_category :subgroups + urgency :low + + deduplicate :until_executing + idempotent! + + def perform(namespace_id) + namespace = Namespace.find_by_id(namespace_id) + return unless namespace + + OnboardingProgressService.new(namespace).execute(action: :pipeline_created) + end + end +end diff --git a/app/workers/object_pool/join_worker.rb b/app/workers/object_pool/join_worker.rb index f1008d3be83..8103c04b507 100644 --- a/app/workers/object_pool/join_worker.rb +++ b/app/workers/object_pool/join_worker.rb @@ -15,7 +15,7 @@ module ObjectPool project.link_pool_repository - Projects::HousekeepingService.new(project).execute + Repositories::HousekeepingService.new(project).execute end end end diff --git a/app/workers/project_update_repository_storage_worker.rb b/app/workers/project_update_repository_storage_worker.rb index 7c0b1ae07fa..5636eec8233 100644 --- a/app/workers/project_update_repository_storage_worker.rb +++ b/app/workers/project_update_repository_storage_worker.rb @@ -1,25 +1,23 @@ # frozen_string_literal: true -class ProjectUpdateRepositoryStorageWorker - include ApplicationWorker +class ProjectUpdateRepositoryStorageWorker # rubocop:disable Scalability/IdempotentWorker + extend ::Gitlab::Utils::Override + include UpdateRepositoryStorageWorker - idempotent! - feature_category :gitaly - urgency :throttled + private - def perform(project_id, new_repository_storage_key, repository_storage_move_id = nil) - repository_storage_move = - if repository_storage_move_id - ProjectRepositoryStorageMove.find(repository_storage_move_id) - else - # maintain compatibility with workers queued before release - project = Project.find(project_id) - project.repository_storage_moves.create!( - source_storage_name: project.repository_storage, - destination_storage_name: new_repository_storage_key - ) - end + override :find_repository_storage_move + def find_repository_storage_move(repository_storage_move_id) + ProjectRepositoryStorageMove.find(repository_storage_move_id) + end + + override :find_container + def find_container(container_id) + Project.find(container_id) + end + override :update_repository_storage + def update_repository_storage(repository_storage_move) ::Projects::UpdateRepositoryStorageService.new(repository_storage_move).execute end end diff --git a/app/workers/snippet_schedule_bulk_repository_shard_moves_worker.rb b/app/workers/snippet_schedule_bulk_repository_shard_moves_worker.rb new file mode 100644 index 00000000000..47f24ad3500 --- /dev/null +++ b/app/workers/snippet_schedule_bulk_repository_shard_moves_worker.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class SnippetScheduleBulkRepositoryShardMovesWorker + include ApplicationWorker + + idempotent! + feature_category :gitaly + urgency :throttled + + def perform(source_storage_name, destination_storage_name = nil) + Snippets::ScheduleBulkRepositoryShardMovesService.new.execute(source_storage_name, destination_storage_name) + end +end diff --git a/app/workers/snippet_update_repository_storage_worker.rb b/app/workers/snippet_update_repository_storage_worker.rb new file mode 100644 index 00000000000..a28a02a0298 --- /dev/null +++ b/app/workers/snippet_update_repository_storage_worker.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class SnippetUpdateRepositoryStorageWorker # rubocop:disable Scalability/IdempotentWorker + extend ::Gitlab::Utils::Override + include UpdateRepositoryStorageWorker + + private + + override :find_repository_storage_move + def find_repository_storage_move(repository_storage_move_id) + SnippetRepositoryStorageMove.find(repository_storage_move_id) + end + + override :find_container + def find_container(container_id) + Snippet.find(container_id) + end + + override :update_repository_storage + def update_repository_storage(repository_storage_move) + ::Snippets::UpdateRepositoryStorageService.new(repository_storage_move).execute + end +end |