diff options
author | Robert Speicher <rspeicher@gmail.com> | 2021-01-20 13:34:23 -0600 |
---|---|---|
committer | Robert Speicher <rspeicher@gmail.com> | 2021-01-20 13:34:23 -0600 |
commit | 6438df3a1e0fb944485cebf07976160184697d72 (patch) | |
tree | 00b09bfd170e77ae9391b1a2f5a93ef6839f2597 /app/assets | |
parent | 42bcd54d971da7ef2854b896a7b34f4ef8601067 (diff) | |
download | gitlab-ce-6438df3a1e0fb944485cebf07976160184697d72.tar.gz |
Add latest changes from gitlab-org/gitlab@13-8-stable-eev13.8.0-rc42
Diffstat (limited to 'app/assets')
1305 files changed, 12791 insertions, 8407 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/graphql/queries/get_pipeline_details.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_details.query.graphql deleted file mode 100644 index 25aede49631..00000000000 --- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_details.query.graphql +++ /dev/null @@ -1,65 +0,0 @@ -#import "../fragments/linked_pipelines.fragment.graphql" - -query getPipelineDetails($projectPath: ID!, $iid: ID!) { - project(fullPath: $projectPath) { - pipeline(iid: $iid) { - id - iid - downstream { - nodes { - ...LinkedPipelineData - } - } - upstream { - ...LinkedPipelineData - } - stages { - nodes { - name - status: detailedStatus { - action { - icon - path - title - } - } - groups { - nodes { - status: detailedStatus { - label - group - icon - } - name - size - jobs { - nodes { - name - scheduledAt - needs { - nodes { - name - } - } - status: detailedStatus { - icon - tooltip - hasDetails - detailsPath - group - action { - buttonTitle - icon - path - title - } - } - } - } - } - } - } - } - } - } -} 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%; -} |