diff options
Diffstat (limited to 'app/assets')
1007 files changed, 21818 insertions, 11259 deletions
diff --git a/app/assets/images/auth_buttons/atlassian_64.png b/app/assets/images/auth_buttons/atlassian_64.png Binary files differnew file mode 100644 index 00000000000..548f1c93318 --- /dev/null +++ b/app/assets/images/auth_buttons/atlassian_64.png diff --git a/app/assets/images/file_icons.svg b/app/assets/images/file_icons.svg index 26ec1a6b388..ec38020f978 100644 --- a/app/assets/images/file_icons.svg +++ b/app/assets/images/file_icons.svg @@ -1 +1 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 24 24" id="actionscript" xmlns="http://www.w3.org/2000/svg"><text style="line-height:113.99999857%" x="5.605" y="15.892" transform="scale(.91325 1.095)" font-weight="400" font-size="42.822" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#f44336"/><path style="line-height:125%" d="M4.744 2.031c-1.157 0-1.994.31-2.51.93-.515.612-.771 1.678-.771 3.197v2.467c0 1.408-.402 2.111-1.201 2.111v2.035c.8 0 1.2.679 1.2 2.036v2.654c0 1.512.26 2.562.78 3.152.52.59 1.355.885 2.502.885V19.43c-.447 0-.77-.151-.97-.453-.195-.303-.292-.815-.292-1.538v-2.267c0-1.807-.404-2.937-1.214-3.395v-.045c.81-.464 1.214-1.581 1.214-3.351V6.025c0-1.283.42-1.925 1.262-1.925V2.03zm14.66 0V4.1c.842 0 1.262.642 1.262 1.925v2.268c0 1.843.402 2.996 1.207 3.46v.046c-.805.442-1.207 1.544-1.207 3.306v2.356c0 .715-.099 1.22-.299 1.516-.2.302-.52.453-.963.453v2.068c1.152 0 1.984-.295 2.494-.885.516-.59.772-1.663.772-3.218V14.84c0-1.379.404-2.069 1.209-2.069v-2.035c-.805 0-1.21-.696-1.21-2.09V6.113c0-1.49-.255-2.54-.77-3.152-.516-.62-1.348-.93-2.495-.93zm-3.054 4.46c-.455 0-.886.057-1.293.173a3.056 3.056 0 0 0-1.078.527c-.308.241-.551.549-.731.924-.18.37-.27.817-.27 1.336 0 .663.165 1.227.493 1.695.33.468.831.864 1.502 1.188.263.125.509.249.736.37.227.12.422.244.586.374.168.13.299.271.394.424a.963.963 0 0 1 .145.521c0 .144-.03.28-.09.405a.9.9 0 0 1-.275.318c-.12.088-.272.158-.455.21a2.34 2.34 0 0 1-.635.075c-.415 0-.825-.083-1.233-.25a3.644 3.644 0 0 1-1.13-.763v2.222a3.68 3.68 0 0 0 1.101.418c.427.093.875.139 1.346.139.459 0 .894-.05 1.305-.152a3.002 3.002 0 0 0 1.09-.5c.31-.237.556-.543.736-.918.183-.38.275-.849.275-1.405 0-.403-.052-.755-.156-1.056a2.542 2.542 0 0 0-.45-.813 3.295 3.295 0 0 0-.704-.633 6.754 6.754 0 0 0-.922-.535 12.4 12.4 0 0 1-.676-.348c-.2-.115-.37-.231-.51-.347a1.502 1.502 0 0 1-.322-.375.91.91 0 0 1-.115-.453c0-.153.033-.288.101-.408a.948.948 0 0 1 .29-.32c.123-.089.275-.156.454-.202a2.18 2.18 0 0 1 .598-.078c.16 0 .326.015.502.043.18.028.36.07.539.13.18.056.354.13.522.218.171.088.329.188.472.304V6.871a4.039 4.039 0 0 0-.957-.285 6.448 6.448 0 0 0-1.185-.096zm-8.774.165l-3.123 9.967h2.094l.605-2.217h3.053l.61 2.217h2.107L9.869 6.656H7.576zm1.072 1.78h.047c.028.347.077.646.145.896l.922 3.35H7.564l.934-3.377c.08-.288.13-.578.15-.87z" font-weight="400" font-size="51.019" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="android" xmlns="http://www.w3.org/2000/svg"><path d="M15 5h-1V4h1m-5 1H9V4h1m5.53-1.84L16.84.85c.19-.19.19-.51 0-.71a.513.513 0 0 0-.71 0l-1.48 1.48C13.85 1.23 12.95 1 12 1c-.96 0-1.86.23-2.66.63L7.85.14a.501.501 0 0 0-.7 0c-.2.2-.2.52 0 .71l1.31 1.31C6.97 3.26 6 5 6 7h12c0-2-1-3.75-2.47-4.84M20.5 8A1.5 1.5 0 0 0 19 9.5v7a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 20.5 8m-17 0A1.5 1.5 0 0 0 2 9.5v7A1.5 1.5 0 0 0 3.5 18 1.5 1.5 0 0 0 5 16.5v-7A1.5 1.5 0 0 0 3.5 8M6 18a1 1 0 0 0 1 1h1v3.5A1.5 1.5 0 0 0 9.5 24a1.5 1.5 0 0 0 1.5-1.5V19h2v3.5a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5V19h1a1 1 0 0 0 1-1V8H6v10z" fill="#c0ca33"/></symbol><symbol viewBox="0 0 24 24" id="angular" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#e53935"/></symbol><symbol viewBox="0 0 24 24" id="angular-component" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#0288d1"/></symbol><symbol viewBox="0 0 24 24" id="angular-directive" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#ab47bc"/></symbol><symbol viewBox="0 0 24 24" id="angular-guard" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#43a047"/></symbol><symbol viewBox="0 0 24 24" id="angular-pipe" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#00897b"/></symbol><symbol viewBox="0 0 24 24" id="angular-resolver" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#43a047"/></symbol><symbol viewBox="0 0 24 24" id="angular-routing" xmlns="http://www.w3.org/2000/svg"><path d="M11 10H5L3 8l2-2h6V3l1-1 1 1v1h6l2 2-2 2h-6v2h6l2 2-2 2h-6v6a2 2 0 0 1 2 2H9a2 2 0 0 1 2-2V10z" fill="#43a047"/></symbol><symbol viewBox="0 0 24 24" id="angular-service" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#ffca28"/></symbol><symbol viewBox="0 0 100 100" id="apiblueprint" xmlns="http://www.w3.org/2000/svg"><title>api-blueprint</title><path d="M50.133 7.521A16.998 16.998 0 0 0 33.135 24.52a16.998 16.998 0 0 0 4.945 11.974L24.861 57.398a16.998 16.998 0 0 0-3.175-.308A16.998 16.998 0 0 0 4.688 74.088a16.998 16.998 0 0 0 16.998 16.998 16.998 16.998 0 0 0 16.998-16.998 16.998 16.998 0 0 0-7.063-13.773l12.576-19.89a16.998 16.998 0 0 0 5.936 1.093 16.998 16.998 0 0 0 6.154-1.155l12.537 19.83a16.998 16.998 0 0 0-7.244 13.895 16.998 16.998 0 0 0 16.998 17 16.998 16.998 0 0 0 16.998-17A16.998 16.998 0 0 0 78.578 57.09a16.998 16.998 0 0 0-2.95.262L62.337 36.327A16.998 16.998 0 0 0 67.13 24.52 16.998 16.998 0 0 0 50.132 7.522z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="applescript" xmlns="http://www.w3.org/2000/svg"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" fill="#78909c"/></symbol><symbol viewBox="0 0 24 24" id="appveyor" xmlns="http://www.w3.org/2000/svg"><path d="M12 2c-.084 0-.165.008-.248.01a10 10 0 0 0-.266.01 9.952 9.952 0 0 0-.754.066 10 10 0 0 0-.148.018 9.855 9.855 0 0 0-.93.177 10 10 0 0 0-.07.02c-.196.049-.392.1-.584.16v.012a10 10 0 0 0-2 .875V3.34c-.02.012-.038.027-.059.039a10 10 0 0 0-.953.635c-.09.067-.172.142-.26.213a10 10 0 0 0-.628.546c-.109.104-.211.211-.315.319a10 10 0 0 0-.476.539c-.1.12-.201.237-.295.361a10 10 0 0 0-.52.766c-.088.143-.17.288-.252.435a10 10 0 0 0-.363.723c-.072.161-.136.327-.2.492a10 10 0 0 0-.269.778c-.02.067-.044.131-.062.199a10 10 0 0 0-.008.027c-.098.364-.166.728-.22 1.09-.012.077-.024.153-.034.23a9.85 9.85 0 0 0-.08 1.182c0 .03-.006.057-.006.086a10 10 0 0 0 .008.148c.001.094-.002.188.002.282l.011.004a10 10 0 0 0 .333 2.158l-.012-.004c.012.047.033.091.047.139a10 10 0 0 0 .322.955c.02.052.037.106.059.158a10 10 0 0 0 .503 1.035c.065.116.14.226.21.34a10 10 0 0 0 .423.64c.092.128.187.252.285.375a10 10 0 0 0 .448.52c.112.123.222.248.341.365a10 10 0 0 0 .803.719 10 10 0 0 0 .01.006c.099.078.207.146.309.22a10 10 0 0 0 .648.442c.138.085.28.163.424.242a10 10 0 0 0 .715.358c.114.051.226.106.343.154a10 10 0 0 0 1.133.389c.016.004.031.01.047.015a10 10 0 0 0 .461.098 10 10 0 0 0 .482.103 10 10 0 0 0 .418.051 10 10 0 0 0 .575.065 10 10 0 0 0 .144.005A10 10 0 0 0 12 22a10 10 0 0 0 .197-.01 10 10 0 0 0 .496-.025 10 10 0 0 0 .49-.043 10 10 0 0 0 .489-.074 10 10 0 0 0 .51-.098 10 10 0 0 0 .47-.12 10 10 0 0 0 .477-.14 10 10 0 0 0 .47-.172 10 10 0 0 0 .481-.197 10 10 0 0 0 .414-.201 10 10 0 0 0 .475-.252 10 10 0 0 0 .39-.238 10 10 0 0 0 .452-.301 10 10 0 0 0 .38-.291 10 10 0 0 0 .385-.315 10 10 0 0 0 .375-.347 10 10 0 0 0 .36-.363 10 10 0 0 0 .293-.334 10 10 0 0 0 .353-.434 10 10 0 0 0 .28-.393 10 10 0 0 0 .263-.4 10 10 0 0 0 .264-.461 10 10 0 0 0 .228-.436 10 10 0 0 0 .195-.437 10 10 0 0 0 .196-.48 10 10 0 0 0 .228-.69 10 10 0 0 0 .028-.094 10 10 0 0 0 .021-.066 10 10 0 0 0 .098-.461 10 10 0 0 0 .103-.482 10 10 0 0 0 .051-.418 10 10 0 0 0 .065-.575 10 10 0 0 0 .005-.144A10 10 0 0 0 22 12a10 10 0 0 0-.01-.197 10 10 0 0 0-.025-.496 10 10 0 0 0-.043-.49 10 10 0 0 0-.074-.489 10 10 0 0 0-.098-.51 10 10 0 0 0-.12-.47 10 10 0 0 0-.14-.477 10 10 0 0 0-.172-.47 10 10 0 0 0-.197-.481 10 10 0 0 0-.201-.414 10 10 0 0 0-.252-.475 10 10 0 0 0-.238-.39 10 10 0 0 0-.301-.452 10 10 0 0 0-.291-.38 10 10 0 0 0-.315-.385 10 10 0 0 0-.347-.375 10 10 0 0 0-.363-.36 10 10 0 0 0-.334-.293 10 10 0 0 0-.434-.353 10 10 0 0 0-.393-.28 10 10 0 0 0-.4-.263 10 10 0 0 0-.461-.264 10 10 0 0 0-.436-.228 10 10 0 0 0-.437-.196 10 10 0 0 0-.48-.195 10 10 0 0 0-.69-.228 10 10 0 0 0-.094-.028 10 10 0 0 0-.066-.021 10 10 0 0 0-.461-.098 10 10 0 0 0-.482-.103 10 10 0 0 0-.418-.051 10 10 0 0 0-.575-.065 10 10 0 0 0-.144-.005A10 10 0 0 0 12 2zm-.016 5.002a5 5 0 0 1 .262.01 5 5 0 0 1 .227.011 5 5 0 0 1 .341.05 5 5 0 0 1 .135.019 5 5 0 0 1 .014.004 5 5 0 0 1 .115.025 5 5 0 0 1 .303.076 5 5 0 0 1 .265.086 5 5 0 0 1 .2.074 5 5 0 0 1 .242.106 5 5 0 0 1 .228.11 5 5 0 0 1 .196.109 5 5 0 0 1 .244.15 5 5 0 0 1 .17.12 5 5 0 0 1 .224.171 5 5 0 0 1 .186.16 5 5 0 0 1 .176.164 5 5 0 0 1 .172.18 5 5 0 0 1 .177.203 5 5 0 0 1 .133.172 5 5 0 0 1 .16.223 5 5 0 0 1 .133.214 5 5 0 0 1 .12.21 5 5 0 0 1 .107.216 5 5 0 0 1 .109.24 5 5 0 0 1 .084.223 5 5 0 0 1 .08.242 5 5 0 0 1 .07.264 5 5 0 0 1 .047.207 5 5 0 0 1 .045.277 5 5 0 0 1 .028.227 5 5 0 0 1 .02.351 5 5 0 0 1 .003.079 5 5 0 0 1-.012.271 5 5 0 0 1-.011.227 5 5 0 0 1-.05.341 5 5 0 0 1-.019.135 5 5 0 0 1-.004.014 5 5 0 0 1-.025.115 5 5 0 0 1-.076.303 5 5 0 0 1-.086.265 5 5 0 0 1-.074.2 5 5 0 0 1-.106.242 5 5 0 0 1-.11.228 5 5 0 0 1-.109.196 5 5 0 0 1-.15.244 5 5 0 0 1-.12.17 5 5 0 0 1-.171.224 5 5 0 0 1-.16.186 5 5 0 0 1-.164.176 5 5 0 0 1-.18.172 5 5 0 0 1-.203.177l-.002.002c-.018.019-.028.035-.047.053l-3.959 5.09-3.05-.979a141.684 141.684 0 0 0 3.177-3.084 5 5 0 0 1-.103-.015 5 5 0 0 1-.149-.024 5 5 0 0 1-.115-.025 5 5 0 0 1-3.57-3.04 5.072 5.072 0 0 1-.206-.661 5 5 0 0 1-.033-.147c-.025-.118-.036-.24-.054-.36-.987.993-1.964 1.993-2.954 3.05l-.98-3.053 5.092-3.957c.043-.044.082-.07.125-.11a5 5 0 0 1 .71-.634c.18-.13.367-.25.561-.356a5 5 0 0 1 .16-.08 4.94 4.94 0 0 1 .516-.222 5 5 0 0 1 .147-.057c.211-.07.43-.123.654-.164a5 5 0 0 1 .172-.027c.236-.035.476-.058.722-.059zM12 9a3 3 0 0 0-.053.002 3 3 0 0 0-.166.01 3 3 0 0 0-.133.011 3 3 0 0 0-.17.026 3 3 0 0 0-.113.021 3 3 0 0 0-.19.05 3 3 0 0 0-.103.03 3 3 0 0 0-.16.057 3 3 0 0 0-.129.053 3 3 0 0 0-.146.072 3 3 0 0 0-.12.063 3 3 0 0 0-.132.082 3 3 0 0 0-.123.08 3 3 0 0 0-.116.088 3 3 0 0 0-.126.105 3 3 0 0 0-.1.094 3 3 0 0 0-.111.111 3 3 0 0 0-.096.107 3 3 0 0 0-.094.116 3 3 0 0 0-.098.136 3 3 0 0 0-.072.11 3 3 0 0 0-.076.133 3 3 0 0 0-.07.132 3 3 0 0 0-.063.14 3 3 0 0 0-.054.14 3 3 0 0 0-.077.228 3 3 0 0 0-.007.026 3 3 0 0 0-.03.138 3 3 0 0 0-.031.149 3 3 0 0 0-.014.11 3 3 0 0 0-.02.183 3 3 0 0 0-.001.052A3 3 0 0 0 9 12a3 3 0 0 0 .002.053 3 3 0 0 0 .01.166 3 3 0 0 0 .011.133 3 3 0 0 0 .026.17 3 3 0 0 0 .021.113 3 3 0 0 0 .05.19 3 3 0 0 0 .03.103 3 3 0 0 0 .057.16 3 3 0 0 0 .053.129 3 3 0 0 0 .072.146 3 3 0 0 0 .063.12 3 3 0 0 0 .082.132 3 3 0 0 0 .08.123 3 3 0 0 0 .088.116 3 3 0 0 0 .105.126 3 3 0 0 0 .094.1 3 3 0 0 0 .111.111 3 3 0 0 0 .107.096 3 3 0 0 0 .116.094 3 3 0 0 0 .136.098 3 3 0 0 0 .11.072 3 3 0 0 0 .133.076 3 3 0 0 0 .132.07 3 3 0 0 0 .135.06 3 3 0 0 0 .153.061 3 3 0 0 0 .216.07 3 3 0 0 0 .004.003 3 3 0 0 0 .026.007 3 3 0 0 0 .138.03 3 3 0 0 0 .149.031 3 3 0 0 0 .11.014 3 3 0 0 0 .183.02 3 3 0 0 0 .011.001 3 3 0 0 0 .041 0A3 3 0 0 0 12 15a3 3 0 0 0 .053-.002 3 3 0 0 0 .166-.01 3 3 0 0 0 .133-.011 3 3 0 0 0 .17-.026 3 3 0 0 0 .113-.021 3 3 0 0 0 .19-.05 3 3 0 0 0 .103-.03 3 3 0 0 0 .16-.057 3 3 0 0 0 .129-.053 3 3 0 0 0 .146-.072 3 3 0 0 0 .12-.063 3 3 0 0 0 .132-.082 3 3 0 0 0 .123-.08 3 3 0 0 0 .116-.088 3 3 0 0 0 .126-.105 3 3 0 0 0 .1-.094 3 3 0 0 0 .111-.111 3 3 0 0 0 .096-.107 3 3 0 0 0 .094-.116 3 3 0 0 0 .098-.136 3 3 0 0 0 .072-.11 3 3 0 0 0 .076-.133 3 3 0 0 0 .07-.132 3 3 0 0 0 .06-.135 3 3 0 0 0 .061-.153 3 3 0 0 0 .07-.216 3 3 0 0 0 .003-.004 3 3 0 0 0 .007-.026 3 3 0 0 0 .03-.138 3 3 0 0 0 .031-.149 3 3 0 0 0 .002-.008 3 3 0 0 0 .012-.101 3 3 0 0 0 .02-.184 3 3 0 0 0 .001-.011 3 3 0 0 0 0-.041A3 3 0 0 0 15 12a3 3 0 0 0-.002-.053 3 3 0 0 0-.01-.166 3 3 0 0 0-.011-.133 3 3 0 0 0-.026-.17 3 3 0 0 0-.021-.113 3 3 0 0 0-.05-.19 3 3 0 0 0-.03-.103 3 3 0 0 0-.057-.16 3 3 0 0 0-.053-.129 3 3 0 0 0-.072-.146 3 3 0 0 0-.063-.12 3 3 0 0 0-.082-.132 3 3 0 0 0-.08-.123 3 3 0 0 0-.088-.116 3 3 0 0 0-.105-.126 3 3 0 0 0-.094-.1 3 3 0 0 0-.111-.111 3 3 0 0 0-.107-.096 3 3 0 0 0-.116-.094 3 3 0 0 0-.136-.098 3 3 0 0 0-.11-.072 3 3 0 0 0-.133-.076 3 3 0 0 0-.132-.07 3 3 0 0 0-.14-.063 3 3 0 0 0-.14-.054 3 3 0 0 0-.228-.077 3 3 0 0 0-.026-.007 3 3 0 0 0-.138-.03 3 3 0 0 0-.149-.031 3 3 0 0 0-.008-.002 3 3 0 0 0-.101-.012 3 3 0 0 0-.184-.02 3 3 0 0 0-.011-.001 3 3 0 0 0-.041 0A3 3 0 0 0 12 9z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 720 720" id="arduino" xmlns="http://www.w3.org/2000/svg"><defs><symbol id="ana" preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg"><path fill="none" stroke-opacity="100%" stroke-width="60" stroke="#00979c" d="M174 30a10.5 10.1 0 0 0 0 280C364 320 344 30 544 30a10.5 10.1 0 0 1 0 280C354 320 374 30 174 30"/><path d="M528 205v-32.8h-32.5v-13.7H528V126h13.9v32.5h32.5v13.7h-32.5V205H528z" text-anchor="middle" fill="#00979c" stroke-width="20" stroke="#00979c" font-family="sans-serif" font-size="167"/><path fill="#00979c" stroke="#00979c" stroke-width="23.6" transform="matrix(1.56 0 0 .64 -366 .528)" d="M321 266v-17.4h53.3V266H321z"/></symbol></defs><title>Layer 1</title><use x="20.063" y="360.85" transform="matrix(.997 0 0 .997 -18.596 -159.19)" xlink:href="#ana"/></symbol><symbol viewBox="0 0 24 24" id="assembly" xmlns="http://www.w3.org/2000/svg"><path d="M1.746 1.566v20.905H5.13v-2.088H3.438V3.656h1.69v-2.09H1.747zm17.219 0v2.09h1.693v16.727h-1.693v2.09h3.383V1.566h-3.383zM15.196 3.988c-.5 0-.93.076-1.29.225-.359.15-.652.372-.877.671-.226.302-.39.673-.494 1.108a6.715 6.715 0 0 0-.155 1.54c0 .573.049 1.083.15 1.528.1.442.264.811.49 1.11.222.298.512.524.872.676.36.153.795.23 1.304.23.518 0 .954-.075 1.308-.224.353-.153.643-.376.869-.671.219-.29.38-.661.484-1.112.104-.454.156-.967.156-1.54 0-.573-.052-1.079-.152-1.515a2.92 2.92 0 0 0-.485-1.106 2.09 2.09 0 0 0-.868-.686c-.354-.155-.79-.234-1.312-.234zm-6.814.12a.941.941 0 0 1-.138.458.849.849 0 0 1-.356.296A1.71 1.71 0 0 1 7.385 5a5.244 5.244 0 0 1-.631.037v1.11H8.19v3.6H6.754v1.188h4.545V9.745H9.894V4.11H8.382zm6.814 1.138c.375 0 .643.176.805.527.161.348.241.933.241 1.756 0 .814-.082 1.399-.247 1.756-.164.356-.43.534-.799.534-.369 0-.636-.178-.8-.534-.165-.357-.248-.941-.248-1.749 0-.829.082-1.415.243-1.763.162-.35.43-.527.805-.527zm-6.33 7.64c-.5 0-.93.073-1.29.223-.359.15-.651.374-.877.673-.225.302-.39.67-.494 1.106a6.715 6.715 0 0 0-.155 1.54c0 .573.05 1.082.15 1.527.1.442.264.814.49 1.112.222.3.514.525.874.677.36.152.793.229 1.302.229.519 0 .954-.076 1.308-.225.354-.153.643-.376.869-.672.22-.29.38-.66.484-1.111.104-.455.156-.967.156-1.54 0-.573-.05-1.079-.15-1.515a2.923 2.923 0 0 0-.487-1.106 2.084 2.084 0 0 0-.867-.686c-.353-.156-.791-.232-1.313-.232zm5.846.119a.941.941 0 0 1-.138.457.85.85 0 0 1-.356.296 1.71 1.71 0 0 1-.503.137 5.245 5.245 0 0 1-.631.037v1.112h1.435v3.597h-1.435v1.189h4.545v-1.189h-1.405v-5.636h-1.512zm-5.846 1.137c.375 0 .643.176.805.527.162.347.241.933.241 1.756 0 .813-.08 1.399-.245 1.755-.164.357-.432.534-.8.534-.37 0-.637-.177-.802-.534-.164-.356-.245-.939-.245-1.746 0-.83.08-1.418.242-1.765.161-.35.43-.527.804-.527z" fill="#ff6e40"/></symbol><symbol viewBox="0 0 24 24" id="aurelia" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="api" x1="-31.824" x2="19.682" y1="-11.741" y2="35.548" gradientTransform="scale(.95818 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#apa"/><linearGradient id="apa" x1="-3.881" x2="2.377" y1="-1.442" y2="4.304"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apj" x1="12.022" x2="-15.716" y1="13.922" y2="-23.952" gradientTransform="scale(.96226 1.0392)" gradientUnits="userSpaceOnUse" xlink:href="#apb"/><linearGradient id="apb" x1=".729" x2="-.971" y1=".844" y2="-1.477"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="apk" x1="-23.39" x2="23.931" y1="-57.289" y2="8.573" gradientTransform="scale(1.0429 .95884)" gradientUnits="userSpaceOnUse" xlink:href="#apc"/><linearGradient id="apc" x1="-2.839" x2="2.875" y1="-6.936" y2="1.017"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apl" x1="-53.331" x2="6.771" y1="-30.517" y2="18.785" gradientTransform="scale(.99898 1.001)" gradientUnits="userSpaceOnUse" xlink:href="#apd"/><linearGradient id="apd" x1="-8.212" x2="1.02" y1="-4.691" y2="2.882"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apm" x1="-14.029" x2="41.998" y1="-23.111" y2="26.259" gradientTransform="scale(1.0003 .99965)" gradientUnits="userSpaceOnUse" xlink:href="#ape"/><linearGradient id="ape" x1="-1.404" x2="4.19" y1="-2.309" y2="2.62"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apn" x1="31.177" x2="3.37" y1="41.442" y2="3.402" gradientTransform="scale(.96254 1.0389)" gradientUnits="userSpaceOnUse" xlink:href="#apf"/><linearGradient id="apf" x1="1.911" x2=".204" y1="2.539" y2=".204"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="apo" x1="-31.905" x2="19.599" y1="-14.258" y2="42.767" gradientTransform="scale(.95823 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#apg"/><linearGradient id="apg" x1="-3.881" x2="2.377" y1="-1.738" y2="5.19"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="app" x1="4.301" x2="34.534" y1="34.41" y2="4.514" gradientTransform="scale(1.002 .99796)" gradientUnits="userSpaceOnUse" xlink:href="#aph"/><linearGradient id="aph" x1=".112" x2=".901" y1=".897" y2=".116"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".53"/><stop stop-color="#CD0F7E" offset=".79"/><stop stop-color="#ED2C89" offset="1"/></linearGradient></defs><g transform="rotate(11.282 -1.694 21.569) scale(.47102)" clip-rule="evenodd" fill="none" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414"><path d="M8.002 6.127L4.117 8.719.116 2.723 4 .13z" transform="rotate(-11.284 17.839 -78.732)" fill="url(#api)"/><path d="M9.179 1.887l6.637 9.946-7.906 5.276-6.637-9.946L.115 5.43 8.02.153z" transform="rotate(-11.284 129.49 -99.884)" fill="url(#apj)"/><path d="M7.3 1.88l1.462 2.189-6.018 4.015L.124 4.16l1.315-.877L6.143.144z" transform="rotate(-11.284 167.2 -62.32)" fill="url(#apk)"/><path d="M2.328 1.146L4.016.02l2.619 3.925L2.75 6.537 1.29 4.347l2.197-1.466zm-1.04 3.201L.132 2.612l2.197-1.466 1.158 1.735z" transform="rotate(-11.284 104.37 -149.22)" fill="url(#apl)"/><path d="M5.346 9.155l-1.315.877L.03 4.035 6.047.019l2.805 4.204L4.15 7.36l4.703-3.138 1.197 1.793z" transform="rotate(-11.284 81.819 7.645)" fill="url(#apm)"/><path d="M14.533 9.934l1.197 1.793-7.907 5.276-1.196-1.793L.052 5.358 7.958.082z" transform="rotate(-11.284 17.141 -7.825)" fill="url(#apn)"/><path d="M6.235 7.177L4.038 8.643 2.84 6.849.036 2.646 3.92.053 7.923 6.05z" transform="rotate(-11.284 18.188 -79.174)" fill="url(#apo)"/><path d="M18.955 35.925L17.48 34.45l3.998-3.998 1.475 1.475z" fill="#714896"/><path d="M33.33 21.55l-1.475-1.474 1.867-1.868 1.475 1.475z" fill="#6f4795"/><path d="M7.12 24.09l-1.525-1.525 3.998-3.998 1.525 1.525z" fill="#88519f"/><path d="M21.495 9.714L19.97 8.19l1.868-1.868 1.524 1.525z" fill="#85509e"/><path d="M31.418 23.462l-6.72 6.72-1.475-1.474 6.72-6.721z" fill="#8d166a"/><path d="M18.058 10.101l1.525 1.525-6.721 6.72-1.525-1.524z" fill="#a70d6f"/><path d="M2.375 11.769l1.9 1.9-1.9 1.901-1.901-1.9z" fill="#9e61ad"/><path d="M15.523 36.482l1.9 1.9-1.9 1.901-1.9-1.9z" fill="#8053a3"/><path d="M8.372 38.294L.017 29.876 29.749.08l8.636 8.201z" transform="translate(1.823 1.548)" fill="url(#app)"/></g></symbol><symbol viewBox="0 0 24 24" id="autohotkey" xmlns="http://www.w3.org/2000/svg"><path d="M5 3c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5zm3.668 3.447a.9.9 0 0 1 .652.256.84.84 0 0 1 .262.625c0 .34-.014.852-.041 1.537-.022.68-.033 1.19-.033 1.53 0 .111-.016.326-.047.644a6.149 6.149 0 0 0-.033.68l2.578-.485c1.007-.179 1.874-.281 2.603-.308.018-.3.048-1.105.088-2.416.01-.345.115-.742.317-1.19.25-.55.533-.826.851-.826.237 0 .448.08.631.236.197.17.295.382.295.637a.775.775 0 0 1-.025.201c-.09.327-.135.612-.135.854 0 .125-.014.32-.041.584-.023.26-.033.453-.033.578 0 .425-.022 1.056-.067 1.893a38.963 38.963 0 0 0-.068 1.892c0 .327.025.816.074 1.465.05.649.074 1.136.074 1.463a.84.84 0 0 1-.261.625.893.893 0 0 1-.65.254 1 1 0 0 1-.686-.254.777.777 0 0 1-.29-.611c0-.327-.015-.818-.046-1.471a39.552 39.552 0 0 1-.041-1.47c0-.256.004-.482.013-.679-.702.032-1.57.142-2.603.33-.86.157-1.719.316-2.578.477-.01.304-.042.812-.096 1.523a22.354 22.354 0 0 0-.066 1.538.84.84 0 0 1-.262.625.893.893 0 0 1-.65.253.898.898 0 0 1-.653-.253.84.84 0 0 1-.262-.625c0-.452.038-1.128.114-2.028.08-.9.12-1.575.12-2.027 0-.573.015-1.436.042-2.586.027-1.155.04-2.017.04-2.59a.84.84 0 0 1 .263-.625.895.895 0 0 1 .65-.256z" fill="#4caf50"/></symbol><symbol viewBox="0 0 24 24" id="autoit" xmlns="http://www.w3.org/2000/svg"><defs id="ardefs8"><style id="arstyle4482">.cls-1{fill:#5d83ac}.cls-2{fill:#f0f0f0;fill-rule:evenodd}</style><style id="arstyle4510">.cls-1{fill:#5d83ac}.cls-2{fill:#f0f0f0;fill-rule:evenodd}</style></defs><g id="arg4522" transform="translate(-59.538 -26.404) scale(.0555)"><path d="M12.8 2.133A10.666 10.666 0 0 0 2.136 12.799 10.666 10.666 0 0 0 12.8 23.465a10.666 10.666 0 0 0 10.668-10.666A10.666 10.666 0 0 0 12.8 2.133zm.15 4.713c.456 0 .836.105 1.142.314.306.21.565.469.78.78l6.089 8.812H9.627l1.82-2.506h3.36c.315 0 .589.01.822.03a11.93 11.93 0 0 1-.473-.663 39.13 39.13 0 0 0-.517-.75l-1.748-2.578-4.577 6.467H4.746l6.25-8.813c.204-.281.46-.534.772-.757.31-.224.705-.336 1.181-.336z" transform="matrix(16.89188 0 0 16.89188 1072.761 475.745)" id="arcircle4514" fill="#1976d2" stroke-width=".026"/></g></symbol><symbol viewBox="0 0 213.33333 213.33333" id="babel" xmlns="http://www.w3.org/2000/svg"><path d="M50.22 199.659c-.875-.406-1.261-1.6-.857-2.652.404-1.053.12-1.914-.63-1.914s-1.615.748-1.92 1.663c-.328.983-1.27.302-2.304-1.667-.962-1.831-3.718-5.533-6.126-8.226-9.418-10.535-7.71-27.444 5.432-53.77 12.459-24.96 23.117-39.033 45.966-60.696 30.229-28.66 52.679-46.223 70.587-55.22 10.98-5.518 13.025-5.059 2.778.624-11.004 6.102-11.378 6.359-10.512 7.226.33.33 7.306-2.67 15.504-6.667 15.87-7.737 16.34-7.912 16.34-6.082 0 .652-4.95 3.738-11 6.858-13.062 6.736-12.722 6.48-10.472 7.872 1.117.69 5.428-.582 11.54-3.406 5.367-2.48 10.397-4.508 11.179-4.508 2.755 0-3.928 5.302-11.541 9.157-20.437 10.35-68.937 46.043-68.07 50.097.166.777-5.792 7.639-13.241 15.248-15.257 15.587-26.14 30.002-33.748 44.706-6.379 12.326-7.457 17.734-5.385 26.996 3.482 15.56 11.592 18.366 31.482 10.895 28.228-10.603 45.758-28.704 47.022-48.556.602-9.442-1.317-13.479-8.52-17.93-4.01-2.48-5.268-2.621-12.065-1.365-4.173.771-10.153 2.906-13.289 4.744s-6.455 3.34-7.377 3.34c-.922 0-3.216 1.336-5.096 2.968-1.88 1.633.48-1.13 5.247-6.14 6.82-7.167 7.956-8.9 5.333-8.132-5.208 1.525-10.194 4.33-15.649 8.803-2.76 2.264-.923.175 4.08-4.641 11.565-11.131 21.183-15.97 33.088-16.641 17.097-.966 27.254 5.805 31.964 21.31 2.435 8.017 2.609 10.24 1.353 17.37-1.65 9.361-7.034 21.553-15.593 35.307-4.398 7.067-8.434 11.427-15.588 16.844-9.166 6.94-15.654 11.02-15.654 9.845 0-.295 2.455-2.161 5.455-4.147 8.818-5.835 5.075-5.377-8.326 1.02-6.854 3.27-15.199 6.593-18.542 7.38-7.106 1.675-30.527 3.164-32.846 2.089zm-8.408-19.899c0-1.1-.6-2-1.333-2-.734 0-1.334.9-1.334 2s.6 2 1.333 2c.734 0 1.334-.9 1.334-2zm89.255-8.204c1.53-1.945 2.473-3.845 2.097-4.222-.377-.377-.836-.435-1.02-.13-.182.306-1.787 2.206-3.565 4.223-1.778 2.016-2.571 3.666-1.763 3.666s2.72-1.591 4.25-3.536zm-77.644-1.745c-.82-2.172-1.74-3.7-2.045-3.396-.951.952 1.088 7.345 2.343 7.345.656 0 .522-1.777-.298-3.95zm82.303-27.915c-.837-.837-3.217 2.55-3.184 4.53.012.734.896.178 1.965-1.235 1.07-1.413 1.618-2.896 1.219-3.295zm-66.238-36.904c-1.312-1.312-3.676.702-3.676 3.133 0 2.035.175 2.031 2.254-.047 1.24-1.24 1.88-2.628 1.422-3.086zm39.657.768c4.403-2.196 6.8-3.986 5.333-3.982-2.838.01-16.667 6.028-16.667 7.254 0 1.6 3.717.527 11.333-3.272zm16.667-5.333c0-.733-.9-1.333-2-1.333s-2 .6-2 1.333.9 1.333 2 1.333 2-.6 2-1.333zm-3.334-3.923l5.334-1.104-7.334-.133c-4.033-.073-8.233.45-9.333 1.16-2.539 1.64 3.572 1.682 11.333.077zm35.738-63.976c2.788-1.69 4.765-3.376 4.393-3.748-.947-.947-11.942 5.654-14.237 8.548-1.792 2.258-1.714 2.276 1.44.329a1452.76 1452.76 0 0 1 8.403-5.13z" fill="#ffca28" stroke-width="1.333"/></symbol><symbol viewBox="0 0 400 400" fill-opacity=".05" id="bithound" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.88 0 0 .88 24.121 2.895)" fill="#e53935" fill-opacity="1"><path d="M370.5 207c-1.5-14.8-4.8-29.9-9.5-44-13.5-40.3-38.6-81.6-70.3-110.1-1.4-1.2-6.7-4.4-8.7-3.3-5.2 2.9 4.6 22.8 5.8 26.4 7.4 22 12.1 45.3 6.8 68.3-7.1 30.4-30.4 51.7-61.5 54.3-17.1 1.4-34.3-.5-51.4 1.5-25.6 3-51.7 11.8-68 32.8-1.9 2.4-3.6 5.1-5.2 7.9h-.4c-6.3.7-12.6-2-15.7-3.7-.8-.5-1.6-.9-2.2-1.2-19-10.5-33-34-41.6-53.4-3.9-9-7.2-18.4-9.3-27.9-1-4.3-1.1-8.8-1.3-13.2-.1-2.7.3-6.5-1.2-8.9-3.3-5.2-7.5-.2-8.2 4-1.1 6.9-2.1 13.7-1.8 20.7.5 11.8 3.8 23.5 8 34.5 6.2 16.2 14.9 31.1 26.2 44.4 4.7 5.5 9.7 10.6 15.1 15.3 4.8 4.3 10.9 7.7 14.5 13.2 4.2 6.3 4.9 14.1 4.5 21.4-1 19.3-1.6 37.4 3.9 56.2 4.8 16.7 10.8 33.8 20.8 48.1 5 7.1 11.2 14.6 18 19.9 4.6 3.6 13.3 4 8.3-9.2-11.1-29.3-12.1-59.7 5.2-87.1 14.5-22.8 40.1-43.1 69-39.5 42.5 5.3 72.1 44.3 70 86-.6 11.7-1 21.7-4.7 32.7-1.5 4.4-2.6 10-1.5 14.6 1.8 7.8 10.5 4.9 14.3-.2 10.3-14 21.1-27.6 30.8-42 31.6-47.2 47-101.8 41.3-158.5z"/><path d="M132.4 92.1c.7 2.3 1.4 4.8 1.9 7.5.1 1.1.4 2.3 1 3.4 2.6 6.8 8.9 10.5 14.8 14 3.6 2.2 10.1 4.3 14.1 5.9 5.2 2.1 16.4-.6 21.7-1 12.2-1 23.5-5.3 34.7 1.2-57.4 67.3-3.2 82.3 38.8 49.9 48-37 2.8-124.3 2.8-124.3s-1-6.8-19.2-10.8c-1.7-.9-3.4-1.7-5.1-2.4-18-8.3-34.2 5.3-47.2 16.4-3.8 3.2-7.5 6.4-11.5 9.4-5.4 4-11.2 7.3-17.3 10.2-6.4 3-14 6.4-21.1 6.7-1 0-2.9.2-4.9.6-3.1.3-4.7 1.1-5.4 2.5-1.2 1-2 2.4-1.8 4.2.2 2.5 1.4 4.6 2.7 6.2.4.1.7.3 1 .4z"/></g></symbol><symbol viewBox="0 0 400.00001 399.99999" id="bower" xmlns="http://www.w3.org/2000/svg"><g transform="translate(12.061 33.203) scale(.81733)"><path d="M447.61 200.08c-23.139-22.234-138.85-36.114-175.36-40.154a107.137 107.137 0 0 0 4.517-12.944 146.107 146.107 0 0 1 15.905-5.901c.677 1.997 3.865 9.648 5.682 13.279 73.415 2.025 77.184-54.557 80.17-70.058 2.92-15.157 2.771-29.802 27.953-56.575-37.516-10.933-91.467 16.945-109.54 58.437-6.79-2.545-13.597-4.424-20.328-5.586-4.824-19.46-29.944-73.672-95.863-73.672-83.46 0-174.43 68.853-174.43 185.41 0 97.976 66.891 183.84 104.68 183.84 16.505 0 30.703-12.36 34.036-23.44 2.795 7.597 11.368 31.213 14.184 37.225 4.162 8.89 23.41 16.583 31.833 7.357 10.83 6.017 30.703 9.641 41.534-6.405 20.86 4.412 39.3-8.026 39.702-22.868 10.235-.546 15.256-14.918 13.021-26.363-1.647-8.426-19.248-38.66-26.113-49.098 13.59 11.054 48.013 14.183 52.194.007 21.911 17.198 56.057 8.171 58.765-5.815 26.624 6.917 57.16-8.276 52.146-26.676 42.771-2.958 37.296-48.464 25.296-59.996z" fill="#543729" stroke-width=".973"/><path d="M328.514 103.025c9.212-18.277 20.788-38.234 35.409-50.58-16.093 6.485-31.981 25.873-41.375 46.595a144.914 144.914 0 0 0-14.552-8.132c13.105-27.972 43.555-51.332 77.112-53.157-22.477 20.385-14.498 62.754-32.979 85.183-5.288-5.311-17.43-15.562-23.615-19.909zm-14.53 29.762c.01-.7.272-6.094.763-8.557-1.288-.304-9.3-1.87-13.476-1.772-.304 5.245 2.204 14.17 4.684 19.541 17.075-.358 29.408-5.471 36.667-10.172-6.18-2.88-16.726-5.442-24.745-6.974-.894 1.851-3.097 6.568-3.892 7.934z" fill="#00acee"/><g stroke-width=".973"><path d="M250.54 277.39c.004.024.015.057.018.082-2.165-4.657-4.463-10.314-7.208-17.708 10.688 15.557 44.184 7.533 42.427-6.407 16.395 12.336 50.143-2.055 42.471-19.353 16.423 7.653 35.168-7.745 30.964-14.455 28 5.4 54.832 10.783 63.256 12.938-5.595 9.123-18.339 15.566-37.549 11.089 10.38 14.14-9.773 31.105-37.844 21.76 6.18 13.883-18.814 26.38-47.22 11.91.361 13.889-35.24 15.488-49.315.143zm55.543-70.194c32.497 2.495 86.238 7.34 119.51 11.997-2.102-10.828-7.844-13.921-25.905-18.772-19.425 2.072-68.706 6.913-93.604 6.776z" fill="#2baf2b"/><path d="M285.78 253.36c16.395 12.336 50.143-2.055 42.471-19.353 16.423 7.653 35.168-7.745 30.964-14.455-33.103-6.383-67.84-12.788-75.719-13.908 4.78.254 12.702.797 22.59 1.556 24.899.137 74.18-4.704 93.604-6.775-31.452-7.975-95.666-19.613-140.01-22.48-2.055 3.003-5.833 8.097-12.413 13.51-19.403 41.053-54.557 68.34-93.454 68.34-11.335 0-24.018-1.912-38.233-6.456-8.865 9.497-46.661 16.694-77.329 1.641 24.326 56.961 80.74 94.984 143.19 94.984 52.591 0 75.912-53.704 70.808-67.914-1.238-3.45-6.145-14.889-8.891-22.283 10.689 15.556 44.185 7.532 42.429-6.408z" fill="#ffcc2f"/><path d="M253.91 145.27c4.644-2.526 20.69-12.253 35.981-15.908a67.843 67.843 0 0 1-.536-5.12c-10.032 2.403-28.945 10.51-39.784-.661 22.866 6.9 34.283-6.149 51.09-6.149 10.014 0 24.305 2.798 35.57 7.22-9.061-8.37-38.772-33.63-75.558-33.717-8.213 9.957-17.09 31.526-6.764 54.334z" fill="#cecece"/><path d="M115.58 253.33c14.215 4.544 26.898 6.457 38.233 6.457 38.896 0 74.05-27.29 93.454-68.341-14.351 11.978-39.291 22.228-78.241 22.228 34.694-7.866 64.56-25.156 79.753-50.427-10.68-16.998-22.263-54.603 7.07-84.33-4.512-14.497-26.475-52.766-75.095-52.766-84.85 0-155.17 71.001-155.17 166.15 0 22.525 4.547 43.65 12.67 62.664 30.666 15.054 68.462 7.858 77.327-1.64z" fill="#ef5734"/><path d="M141.03 108.45c0 21.644 17.546 39.191 39.19 39.191s39.192-17.548 39.192-39.191c0-21.644-17.548-39.191-39.192-39.191-21.644 0-39.19 17.547-39.19 39.191z" fill="#ffcc2f"/><path d="M156.76 108.45c0 12.958 10.507 23.463 23.463 23.463 12.96 0 23.464-10.506 23.464-23.463 0-12.959-10.504-23.464-23.464-23.464-12.957 0-23.463 10.506-23.463 23.464z" fill="#543729"/><ellipse cx="180.22" cy="98.044" rx="13.673" ry="8.501" fill="#fff"/></g></g></symbol><symbol viewBox="0 0 140 140" id="browserlist" xmlns="http://www.w3.org/2000/svg"><title>Browserslist logo</title><path d="M70.314 10.066a59.828 59.828 0 0 0-59.828 59.828 59.828 59.828 0 0 0 59.828 59.828 59.828 59.828 0 0 0 59.828-59.828 59.828 59.828 0 0 0-59.828-59.828zm-4.836 8.785c.496 4.043 1.352 7.322 2.572 10.223 4.779-4.287 10.265-7.546 16.041-9.02-.981 3.938-1.357 7.295-1.261 10.43 6.026-2.314 12.349-3.404 18.3-2.706-3.182 2.413-5.482 4.717-7.128 7.015-2.201 12.074 6.858 20.43 14.779 24.551a5.128 5.128 0 0 1 5.183-3.888 5.128 5.128 0 0 1 3.7 8.435v.002c-.487 1.055-2.002 2.343-3.497 3.219-4.075 2.39-11.172 5.736-20.914 7.39.045 1.214.077 2.453.077 3.747 0 4.817-.485 8.291-1.385 10.699-3.3 13.313-12.648 26.76-24.695 31.95.357-4.083.197-7.485-.402-10.591-5.582 3.218-11.646 5.278-17.623 5.52h-.002c1.785-3.662 2.855-6.878 3.412-9.976-6.347.996-12.727.742-18.377-1.17 2.93-2.732 5.054-5.314 6.673-7.96-6.292-1.344-12.169-3.87-16.766-7.686 3.822-1.544 6.795-3.239 9.3-5.197-5.426-3.517-10.034-7.998-12.972-13.23 4.012-.07 7.321-.568 10.3-1.453-3.786-5.215-6.468-11.032-7.333-16.951 3.861 1.405 7.196 2.133 10.36 2.355-1.662-6.22-2.081-12.605-.768-18.436 3.03 2.634 5.824 4.48 8.63 5.815.678-6.406 2.576-12.52 5.893-17.496 1.926 3.622 3.914 6.391 6.111 8.672 2.93-5.754 6.9-10.798 11.791-14.262zm26.465 19.557c-2.395 5.514-1.665 11.297-.555 18.732a2.138 2.138 0 0 0 .28-4.178 3.419 3.419 0 1 1 .092 6.704c.574 3.882 1.157 8.18 1.421 13.125a67.143 67.143 0 0 0 3.25-.649c6.616-1.487 12.258-3.801 16.871-6.506.45-.264.884-.563 1.276-.867.366-.557.333-.957.035-1.285-4.831-1.245-10.891-4.53-15.258-8.795-4.764-4.653-7.428-10.164-7.412-16.281z" fill="#ffca28" stroke-width=".855"/></symbol><symbol viewBox="0 0 140 140" id="browserlist_light" xmlns="http://www.w3.org/2000/svg"><title>Browserslist logo</title><g transform="translate(10.823 10.1)" stroke-width=".855"><circle cx="59.492" cy="59.795" r="59.828" fill="#ffca28"/><path d="M54.656 8.752c-4.89 3.464-8.862 8.508-11.791 14.262-2.198-2.28-4.185-5.05-6.111-8.672-3.318 4.976-5.216 11.09-5.893 17.496-2.807-1.335-5.6-3.18-8.63-5.814-1.314 5.83-.895 12.216.767 18.436-3.164-.223-6.498-.95-10.36-2.356.865 5.92 3.548 11.737 7.333 16.951-2.978.885-6.287 1.383-10.3 1.453 2.939 5.233 7.547 9.714 12.972 13.23-2.505 1.959-5.478 3.654-9.299 5.198 4.596 3.815 10.474 6.341 16.766 7.685-1.62 2.647-3.743 5.228-6.674 7.96 5.65 1.912 12.03 2.166 18.377 1.17-.556 3.098-1.626 6.314-3.412 9.975h.002c5.977-.24 12.042-2.3 17.623-5.52.6 3.108.76 6.51.402 10.593 12.047-5.19 21.395-18.638 24.695-31.951.9-2.408 1.385-5.881 1.385-10.7 0-1.293-.031-2.531-.076-3.745 9.742-1.655 16.839-5.001 20.914-7.39 1.494-.877 3.01-2.165 3.496-3.22v-.002a5.128 5.128 0 0 0-3.7-8.435 5.128 5.128 0 0 0-5.183 3.889c-7.92-4.122-16.98-12.477-14.779-24.551 1.646-2.299 3.947-4.603 7.13-7.016-5.952-.698-12.276.392-18.302 2.707-.095-3.135.28-6.492 1.262-10.43-5.776 1.473-11.262 4.733-16.041 9.02-1.22-2.902-2.076-6.18-2.572-10.223zm26.465 19.557c-.015 6.117 2.648 11.628 7.412 16.281 4.366 4.265 10.426 7.55 15.258 8.795.298.328.331.728-.035 1.285-.392.304-.825.603-1.275.867-4.613 2.704-10.256 5.019-16.871 6.506-1.071.24-2.154.458-3.25.649-.265-4.945-.848-9.243-1.422-13.125a3.419 3.419 0 1 0-.092-6.703 2.138 2.138 0 0 1-.28 4.177c-1.11-7.435-1.84-13.218.555-18.732z" fill="#37474f"/></g></symbol><symbol viewBox="0 0 24 24" id="bucklescript" xmlns="http://www.w3.org/2000/svg"><path d="M3 3v18h18V3H3zm14.1 8.858a5.5 5.5 0 0 1 1.26.145c.417.093.778.213 1.082.357v1.723h-.18a3.281 3.281 0 0 0-.959-.603 2.867 2.867 0 0 0-1.155-.247c-.14 0-.277.011-.416.035a1.4 1.4 0 0 0-.395.12.756.756 0 0 0-.291.231.54.54 0 0 0-.123.348c0 .198.065.35.196.456.13.104.376.2.738.288.237.057.466.11.683.164.22.054.455.128.706.222.496.188.86.444 1.095.77.238.32.357.738.357 1.253 0 .737-.271 1.336-.813 1.798-.538.46-1.27.689-2.197.689a5.447 5.447 0 0 1-1.402-.161 6.725 6.725 0 0 1-1.117-.416v-1.794h.183c.344.318.73.563 1.155.734.429.17.839.256 1.233.256.1 0 .235-.01.4-.03.166-.02.3-.055.403-.102a.97.97 0 0 0 .313-.225c.084-.09.127-.223.127-.4a.568.568 0 0 0-.183-.424c-.119-.12-.294-.213-.526-.276-.243-.067-.5-.128-.773-.185a5.523 5.523 0 0 1-.76-.227c-.544-.204-.936-.48-1.177-.828-.237-.351-.357-.786-.357-1.305 0-.697.27-1.265.81-1.703.54-.442 1.235-.663 2.083-.663zm-8.981.135h2.51c.521 0 .903.02 1.143.06.243.041.484.13.721.266.246.144.43.338.548.583.121.24.181.518.181.83 0 .36-.082.68-.247.959a1.697 1.697 0 0 1-.7.642v.04c.423.098.758.298 1.004.603.249.305.373.706.373 1.205 0 .361-.063.686-.19.97-.125.285-.296.52-.516.707a2.31 2.31 0 0 1-.845.472c-.304.094-.69.141-1.159.141H8.12v-7.478zm1.659 1.372v1.582h.262c.263 0 .486-.007.672-.017.185-.01.332-.043.44-.1.15-.077.248-.175.294-.295.046-.124.07-.266.07-.427a.91.91 0 0 0-.083-.371.518.518 0 0 0-.282-.277 1.187 1.187 0 0 0-.456-.086c-.18-.007-.433-.01-.76-.01h-.157zm0 2.873V18.1H9.9c.469 0 .804-.002 1.007-.006.202-.003.39-.046.56-.13a.712.712 0 0 0 .357-.33c.067-.142.099-.302.099-.483 0-.237-.04-.42-.121-.547-.078-.13-.214-.228-.405-.291a1.842 1.842 0 0 0-.538-.072 49.47 49.47 0 0 0-.716-.003h-.366z" fill="#26a69a" stroke-width="1.067"/></symbol><symbol viewBox="0 0 24 24" id="c" xmlns="http://www.w3.org/2000/svg"><path d="M15.45 15.97l.42 2.44c-.26.14-.68.27-1.24.39-.57.13-1.24.2-2.01.2-2.21-.04-3.87-.7-4.98-1.96-1.14-1.27-1.68-2.88-1.68-4.83C6 9.9 6.68 8.13 8 6.89 9.28 5.64 10.92 5 12.9 5c.75 0 1.4.07 1.94.19s.94.25 1.2.4l-.6 2.49-1.04-.34c-.4-.1-.87-.15-1.4-.15-1.15-.01-2.11.36-2.86 1.1-.76.73-1.14 1.85-1.18 3.34.01 1.36.37 2.42 1.08 3.2.71.77 1.7 1.17 2.99 1.18l1.33-.12c.43-.08.79-.19 1.09-.32z" fill="#0277bd"/></symbol><symbol viewBox="0 0 300 300" id="cabal" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -822.52)" fill-rule="evenodd" color="#000"><rect transform="matrix(-.98339 .18149 .60192 .79856 0 0)" x="405.55" y="967.22" width="107.25" height="156.59" rx="12.306" ry="12.31" fill="#2d9bbd"/><rect transform="matrix(-.98528 .17093 -.59175 .80612 0 0)" x="-1156.5" y="1461.9" width="108.34" height="123.15" rx="10.69" ry="12.31" fill="#4a4bcd"/><path d="M52.112 965.158c-1.343 3.515-26.292 23.248-25.744 27.277.548 4.03 29.812 16.023 32.04 19.027s10.545 41.668 13.603 42.5 18.828-31.274 21.548-32.932c2.72-1.658 32.808 2.503 34.15-1.01 1.343-3.515-18.174-35.352-18.721-39.381-.548-4.03 9.732-40.12 7.502-43.125-2.229-3.005-30.06 9.427-33.118 8.594-3.059-.833-26.793-27.3-29.514-25.643-2.72 1.657-.405 41.177-1.747 44.693z" fill="#2e5bc1"/></g></symbol><symbol viewBox="0 0 24 24" id="cake" xmlns="http://www.w3.org/2000/svg"><path d="M12.254 6.621a1.807 1.807 0 0 0 1.808-1.807c0-.344-.09-.66-.262-.932l-1.546-2.684-1.546 2.684a1.72 1.72 0 0 0-.262.932 1.808 1.808 0 0 0 1.808 1.807m4.158 9.04l-.967-.976-.976.976c-1.175 1.166-3.236 1.175-4.42 0l-.959-.976-.994.976a3.134 3.134 0 0 1-3.977.353v4.167a.904.904 0 0 0 .904.904h14.463a.904.904 0 0 0 .904-.904v-4.167a3.134 3.134 0 0 1-3.977-.353m1.265-6.328h-4.52V7.525H11.35v1.808H6.83a2.712 2.712 0 0 0-2.711 2.712v1.392c0 .977.795 1.772 1.771 1.772.489 0 .94-.18 1.248-.515l1.952-1.926 1.908 1.926c.669.669 1.835.669 2.504 0l1.916-1.926 1.944 1.926c.316.334.768.515 1.247.515.976 0 1.78-.795 1.78-1.772v-1.392a2.712 2.712 0 0 0-2.711-2.712z" fill="#ff7043" stroke-width=".904"/></symbol><symbol viewBox="0 0 24 24" id="certificate" xmlns="http://www.w3.org/2000/svg"><path d="M4 3c-1.11 0-2 .89-2 2v10a2 2 0 0 0 2 2h8v5l3-3 3 3v-5h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H4m8 2l3 2 3-2v3.5l3 1.5-3 1.5V15l-3-2-3 2v-3.5L9 10l3-1.5V5M4 5h5v2H4V5m0 4h3v2H4V9m0 4h5v2H4v-2z" fill="#ff5722"/></symbol><symbol viewBox="0 0 24 24" id="changelog" xmlns="http://www.w3.org/2000/svg"><path d="M11 7v5.11l4.71 2.79.79-1.28-4-2.37V7m0-5C8.97 2 5.91 3.92 4.27 6.77L2 4.5V11h6.5L5.75 8.25C6.96 5.73 9.5 4 12.5 4a7.5 7.5 0 0 1 7.5 7.5 7.5 7.5 0 0 1-7.5 7.5c-3.27 0-6.03-2.09-7.06-5h-2.1c1.1 4.03 4.77 7 9.16 7 5.24 0 9.5-4.25 9.5-9.5A9.5 9.5 0 0 0 12.5 2z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 24 24" id="clojure" xmlns="http://www.w3.org/2000/svg"><path d="M3.355 1.78c-.845 0-1.525.68-1.525 1.525v17.441c0 .845.68 1.525 1.525 1.525h17.442c.845 0 1.525-.68 1.525-1.525V3.305c0-.845-.68-1.526-1.525-1.526H3.355zm6.168 2.572h1.963l6.368 14.931H15.93l-3.38-8.086-3.349 8.086H7.21l4.346-10.38-2.032-4.551z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="cmake" xmlns="http://www.w3.org/2000/svg"><path d="M11.99 2.965L2.977 20.999l9.874-8.47-.863-9.564z" fill="#1e88e5"/><path d="M12.007 2.963l.002.29 1.312 14.498-.001.006.023.26 7.362 2.979h.416l-.158-.311-.114-.228h-.002l-8.84-17.494z" fill="#e53935"/><path d="M8.607 16.11L2.98 20.995h17.743v-.016L8.607 16.11z" fill="#7cb342"/></symbol><symbol class="bfmain_logo__svg" viewBox="0 0 300 300.00001" id="code-climate" xmlns="http://www.w3.org/2000/svg"><path class="bfsymbol" d="M196.19 75.562l-51.846 51.561 30.766 30.766 21.08-21.08 59.252 59.537 30.481-30.766zm-61.246 60.961l-30.481-30.481-78.053 78.053-11.964 11.964 30.766 30.766 11.964-12.249 39.596-39.312 7.691-7.691 30.481 30.48 28.772 28.773 30.766-30.766-28.772-28.772z" fill="#eee" stroke-width="2.849"/></symbol><symbol class="bgmain_logo__svg" viewBox="0 0 300 300.00001" id="code-climate_light" xmlns="http://www.w3.org/2000/svg"><path class="bgsymbol" d="M196.19 75.562l-51.846 51.561 30.766 30.766 21.08-21.08 59.252 59.537 30.481-30.766zm-61.246 60.961l-30.481-30.481-78.053 78.053-11.964 11.964 30.766 30.766 11.964-12.249 39.596-39.312 7.691-7.691 30.481 30.48 28.772 28.773 30.766-30.766-28.772-28.772z" fill="#455a64" stroke-width="2.849"/></symbol><symbol viewBox="0 0 24 24" id="coffee" xmlns="http://www.w3.org/2000/svg"><path d="M2 21h18v-2H2M20 8h-2V5h2m0-2H4v10a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4v-3h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="coldfusion" xmlns="http://www.w3.org/2000/svg"><rect transform="rotate(90)" x="2.283" y="-21.86" width="19.487" height="19.487" ry="0" fill="#0d3858" stroke="#4dd0e1" stroke-width=".7"/><text x="6.653" y="16.426" fill="#4dd0e1" font-family="Calibri" font-size="29.001" font-weight="bold" letter-spacing="0" stroke-width=".725" word-spacing="0" style="line-height:1.25"><tspan x="6.653" y="16.426" font-family="'Segoe UI'" font-size="10.634" font-weight="normal">C<tspan font-size="11.844">f</tspan></tspan></text></symbol><symbol viewBox="0 0 24 24" id="conduct" xmlns="http://www.w3.org/2000/svg"><path d="M10 17l-4-4 1.41-1.41L10 14.17l6.59-6.59L18 9m-6-6a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#cddc39"/></symbol><symbol viewBox="0 0 24 24" id="console" xmlns="http://www.w3.org/2000/svg"><path d="M20 19V7H4v12h16m0-16a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16m-7 14v-2h5v2h-5m-3.42-4L5.57 9H8.4l3.3 3.3c.39.39.39 1.03 0 1.42L8.42 17H5.59z" fill="#ff7043"/></symbol><symbol viewBox="0 0 24 24" id="contributing" xmlns="http://www.w3.org/2000/svg"><path d="M17 9H7V7h10m0 6H7v-2h10m-3 6H7v-2h7M12 3a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="cpp" xmlns="http://www.w3.org/2000/svg"><path d="M10.5 15.97l.41 2.44c-.26.14-.68.27-1.24.39-.57.13-1.24.2-2.01.2-2.21-.04-3.87-.7-4.98-1.96C1.56 15.77 1 14.16 1 12.21c.05-2.31.72-4.08 2-5.32C4.32 5.64 5.96 5 7.94 5c.75 0 1.4.07 1.94.19s.94.25 1.2.4l-.58 2.49-1.06-.34c-.4-.1-.86-.15-1.39-.15-1.16-.01-2.12.36-2.87 1.1-.76.73-1.15 1.85-1.18 3.34 0 1.36.37 2.42 1.08 3.2.71.77 1.71 1.17 2.99 1.18l1.33-.12c.43-.08.79-.19 1.1-.32M11 11h2V9h2v2h2v2h-2v2h-2v-2h-2v-2m7 0h2V9h2v2h2v2h-2v2h-2v-2h-2v-2z" fill="#0277bd"/></symbol><symbol viewBox="0 0 24 24" id="credits" xmlns="http://www.w3.org/2000/svg"><path d="M3 3h18v2H3V3m4 4h10v2H7V7m-4 4h18v2H3v-2m4 4h10v2H7v-2m-4 4h18v2H3v-2z" fill="#9ccc65"/></symbol><symbol viewBox="0 0 200 200" id="crystal" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:none}</style><path d="M179.363 121.67l-57.623 57.507c-.23.23-.576.346-.806.23l-78.713-21.09c-.346-.115-.577-.345-.577-.576L20.44 79.144c-.115-.345 0-.576.23-.806L78.294 20.83c.23-.23.576-.346.807-.23l78.713 21.09c.345.114.576.345.576.575l21.09 78.597c.23.346.115.577-.115.807zM102.148 59.09l-77.33 20.63c-.115 0-.23.23-.115.345l56.586 56.47c.115.115.346.115.346-.115l20.744-77.215c.115 0-.115-.23-.23-.116z" stroke-width="1.153" fill="#cfd8dc"/></symbol><symbol viewBox="0 0 200 200" id="crystal_light" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:none}</style><path d="M179.363 121.67l-57.623 57.507c-.23.23-.576.346-.806.23l-78.713-21.09c-.346-.115-.577-.345-.577-.576L20.44 79.144c-.115-.345 0-.576.23-.806L78.294 20.83c.23-.23.576-.346.807-.23l78.713 21.09c.345.114.576.345.576.575l21.09 78.597c.23.346.115.577-.115.807zM102.148 59.09l-77.33 20.63c-.115 0-.23.23-.115.345l56.586 56.47c.115.115.346.115.346-.115l20.744-77.215c.115 0-.115-.23-.23-.116z" fill="#37474f" stroke-width="1.153"/></symbol><symbol viewBox="0 0 24 24" id="csharp" xmlns="http://www.w3.org/2000/svg"><path d="M11.5 15.97l.41 2.44c-.26.14-.68.27-1.24.39-.57.13-1.24.2-2.01.2-2.21-.04-3.87-.7-4.98-1.96C2.56 15.77 2 14.16 2 12.21c.05-2.31.72-4.08 2-5.32C5.32 5.64 6.96 5 8.94 5c.75 0 1.4.07 1.94.19s.94.25 1.2.4l-.58 2.49-1.06-.34c-.4-.1-.86-.15-1.39-.15-1.16-.01-2.12.36-2.87 1.1-.76.73-1.15 1.85-1.18 3.34 0 1.36.37 2.42 1.08 3.2.71.77 1.71 1.17 2.99 1.18l1.33-.12c.43-.08.79-.19 1.1-.32M13.89 19l.61-4H13l.34-2h1.5l.32-2h-1.5L14 9h1.5l.61-4h2l-.61 4h1l.61-4h2l-.61 4H22l-.34 2h-1.5l-.32 2h1.5L21 15h-1.5l-.61 4h-2l.61-4h-1l-.61 4h-2m2.95-6h1l.32-2h-1l-.32 2z" fill="#0277bd"/></symbol><symbol viewBox="0 0 24 24" id="css" xmlns="http://www.w3.org/2000/svg"><path d="M5 3l-.65 3.34h13.59L17.5 8.5H3.92l-.66 3.33h13.59l-.76 3.81-5.48 1.81-4.75-1.81.33-1.64H2.85l-.79 4 7.85 3 9.05-3 1.2-6.03.24-1.21L21.94 3H5z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="css-map" xmlns="http://www.w3.org/2000/svg"><path d="M18 8v2h2v10H10v-2H8v4h14V8h-4z" fill="#42a5f5"/><path d="M4.676 3l-.488 2.51h10.211l-.33 1.623H3.864l-.496 2.502H13.58l-.57 2.863-4.119 1.36-3.569-1.36.248-1.232H3.06l-.593 3.005 5.898 2.254 6.8-2.254.902-4.53.18-.91L17.406 3H4.675z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 33 33" id="cucumber" xmlns="http://www.w3.org/2000/svg"><title>cucumber-mark-transparent-pips</title><g transform="translate(0 -5)" fill="none" fill-rule="evenodd"><path d="M-4-1h40v40H-4z"/><path d="M16.641 7.092c-7.028 0-12.714 5.686-12.714 12.714 0 6.187 4.435 11.327 10.288 12.471v3.64C21.824 34.77 28.561 28.73 29.063 20.8c.303-4.772-2.076-9.644-6.09-12.01a10.575 10.575 0 0 0-1.455-.728l-.243-.097c-.223-.082-.448-.175-.68-.242a12.614 12.614 0 0 0-3.954-.632zm2.62 4.707a1.387 1.387 0 0 0-1.213.485c-.233.31-.379.611-.534.923-.466 1.087-.31 2.251.388 3.105 1.087-.233 2.01-.927 2.475-2.014a2.45 2.45 0 0 0 .243-1.02c.048-.824-.634-1.404-1.359-1.479zm-5.654.073c-.708.068-1.382.63-1.382 1.407 0 .31.087.709.243 1.02.466 1.086 1.46 1.78 2.546 2.013.621-.854.782-2.018.316-3.105-.155-.311-.3-.617-.534-.85a1.364 1.364 0 0 0-1.188-.485zm-3.809 3.735c-1.224.063-1.77 1.602-.752 2.402.31.233.612.403.922.559 1.087.466 2.344.306 3.275-.316-.233-1.009-1.023-1.936-2.11-2.402-.388-.155-.703-.243-1.092-.243-.087-.009-.161-.004-.243 0zm11.961 4.708a3.551 3.551 0 0 0-2.013.582c.233 1.01 1.023 1.936 2.11 2.401.389.156.705.244 1.093.244 1.397.077 2.08-1.65.994-2.427-.31-.233-.611-.379-.922-.534a3.354 3.354 0 0 0-1.262-.266zm-10.603.072a3.376 3.376 0 0 0-1.261.267c-.389.155-.69.325-.923.558-1.009.854-.33 2.48 1.068 2.402.388 0 .782-.087 1.092-.243 1.087-.465 1.859-1.392 2.014-2.401a3.474 3.474 0 0 0-1.99-.582zm3.931 2.378c-1.087.233-2.009.927-2.475 2.014-.155.31-.243.684-.243.995-.077 1.32 1.724 2.028 2.5 1.02.233-.312.378-.613.534-.923.466-1.01.306-2.174-.316-3.106zm2.887.073c-.621.854-.781 2.019-.315 3.106.155.31.3.615.534.848.854.932 2.65.243 2.572-.921 0-.31-.088-.71-.243-1.02-.466-1.087-1.46-1.78-2.547-2.013z" fill="#4caf50" stroke-width=".776"/></g></symbol><symbol id="cuda" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><style>.bust0{fill:#76b900}</style><title>NVIDIA-Logo</title><path id="buEye_Mark" class="bust0" d="M76.362 75.199V64.116c1.095-.068 2.19-.137 3.284-.137 30.377-.958 50.286 26.135 50.286 26.135s-21.483 29.83-44.539 29.83c-3.079 0-6.089-.48-8.962-1.438v-33.66c11.836 1.436 14.23 6.636 21.277 18.471l15.804-13.273s-11.562-15.12-30.992-15.12c-2.053-.068-4.105.069-6.158.274m0-36.67v16.556l3.284-.205c42.213-1.437 69.784 34.618 69.784 34.618s-31.608 38.45-64.516 38.45c-2.873 0-5.678-.274-8.483-.753v10.262c2.326.274 4.72.48 7.046.48 30.65 0 52.817-15.668 74.3-34.14 3.558 2.874 18.13 9.784 21.14 12.794-20.388 17.104-67.937 30.856-94.893 30.856-2.6 0-5.062-.137-7.525-.41v14.436h116.44V38.532zm0 79.977v8.757C48.038 122.2 40.17 92.712 40.17 92.712s13.615-15.05 36.192-17.514v9.579h-.068c-11.836-1.437-21.14 9.646-21.14 9.646s5.268 18.678 21.209 24.082M26.077 91.481S42.839 66.714 76.43 64.115v-9.03C39.213 58.094 7.057 89.565 7.057 89.565s18.199 52.68 69.305 57.47v-9.579c-37.492-4.652-50.286-45.975-50.286-45.975z" fill="#8bc34a" stroke-width=".684"/></symbol><symbol viewBox="0 0 24 24" id="dart" xmlns="http://www.w3.org/2000/svg"><title>Dart</title><path d="M12.486 1.385a.978.978 0 0 0-.682.281l-.01.007-6.387 3.692 6.371 6.372v.004l7.659 7.659 1.46-2.63-5.265-12.64-2.456-2.457a.972.972 0 0 0-.69-.288z" fill="#00ca94"/><path d="M5.422 5.35L1.73 11.733l-.007.01a.967.967 0 0 0 .006 1.371l3.059 3.061 11.963 4.706 2.704-1.502-.073-.073-.018.002-7.5-7.512h-.01L5.423 5.35z" fill="#1565c0"/><path d="M5.405 5.353l6.518 6.525h.01l7.502 7.51 2.855-.544.005-8.449-3.016-2.955c-.66-.647-1.675-1.064-2.695-1.202l.002-.032-11.181-.853z" fill="#1565c0"/><path d="M5.414 5.361l6.521 6.522v.009l7.506 7.506-.546 2.855h-8.448l-2.954-3.017c-.647-.66-1.064-1.676-1.2-2.696l-.033.003L5.414 5.36z" fill="#00ee94"/></symbol><symbol viewBox="0 0 24 24" id="database" xmlns="http://www.w3.org/2000/svg"><path d="M12 3C7.58 3 4 4.79 4 7s3.58 4 8 4 8-1.79 8-4-3.58-4-8-4M4 9v3c0 2.21 3.58 4 8 4s8-1.79 8-4V9c0 2.21-3.58 4-8 4s-8-1.79-8-4m0 5v3c0 2.21 3.58 4 8 4s8-1.79 8-4v-3c0 2.21-3.58 4-8 4s-8-1.79-8-4z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="diff" xmlns="http://www.w3.org/2000/svg"><path d="M3 1c-1.11 0-2 .89-2 2v11c0 1.11.89 2 2 2h2v-2H3V3h11v2h2V3c0-1.11-.89-2-2-2H3m6 6c-1.11 0-2 .89-2 2v2h2V9h2V7H9m4 0v2h1v1h2V7h-3m5 0v2h2v11H9v-2H7v2c0 1.11.89 2 2 2h11c1.11 0 2-.89 2-2V9c0-1.11-.89-2-2-2h-2m-4 5v2h-2v2h2c1.11 0 2-.89 2-2v-2h-2m-7 1v3h3v-2H9v-1H7z" fill="#42a5f5"/></symbol><symbol id="docker" viewBox="0 0 41 34.5" xmlns="http://www.w3.org/2000/svg"><style id="bystyle2">.byst0{fill:#fff}.byst1{clip-path:url(#bySVGID_4_)}</style><g id="byg34" transform="translate(.292 1.9)" fill="#0087c9"><g id="byg32"><g id="byg30"><title id="bytitle4">Group 3</title><g id="byg28"><g id="byg26"><g id="byg9"><path id="bySVGID_1_" class="byst0" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2c1.2 0 2.1.9 2.1 2s-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></g><g id="byg24"><defs id="bydefs12"><path id="bySVGID_2_" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2c1.2 0 2.1.9 2.1 2s-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></defs><clipPath id="bySVGID_4_"><use xlink:href="#bySVGID_2_" id="byuse14" width="100%" height="100%" overflow="visible"/></clipPath><g class="byst1" clip-path="url(#bySVGID_4_)" id="byg22"><g id="byg20"><g id="byg18"><path id="bySVGID_3_" class="byst0" d="M-48.8-21H1226v151.4H-48.8z"/></g></g></g></g></g></g></g></g></g></symbol><symbol viewBox="0 0 24 24" id="document" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m9 16v-2H6v2h9m3-4v-2H6v2h12z" fill="#42a5f5"/></symbol><symbol preserveAspectRatio="xMidYMid" viewBox="0 0 200 200" id="drone" xmlns="http://www.w3.org/2000/svg"><g fill="#e0e0e0" transform="translate(9.063 22.346) scale(.71044)"><path d="M128.22.723C32.095.723.39 84.566.39 115.222h77.928S89.36 75.275 128.22 75.275s49.906 39.947 49.906 39.947h77.476c0-30.66-31.257-114.5-127.38-114.5m98.82 134.45h-48.914s-8.55 39.946-49.906 39.946c-41.355 0-49.902-39.948-49.902-39.948H30.255c0 10.25 37.727 82.708 98.443 82.708 60.714 0 98.344-59.604 98.344-82.708"/><circle cx="128" cy="126.08" r="32.768"/></g></symbol><symbol preserveAspectRatio="xMidYMid" viewBox="0 0 200 200" id="drone_light" xmlns="http://www.w3.org/2000/svg"><g fill="#424242" transform="translate(9.063 22.346) scale(.71044)"><path d="M128.22.723C32.095.723.39 84.566.39 115.222h77.928S89.36 75.275 128.22 75.275s49.906 39.947 49.906 39.947h77.476c0-30.66-31.257-114.5-127.38-114.5m98.82 134.45h-48.914s-8.55 39.946-49.906 39.946c-41.355 0-49.902-39.948-49.902-39.948H30.255c0 10.25 37.727 82.708 98.443 82.708 60.714 0 98.344-59.604 98.344-82.708"/><circle cx="128" cy="126.08" r="32.768"/></g></symbol><symbol viewBox="0 0 3473 3473" id="editorconfig" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" xmlns="http://www.w3.org/2000/svg"><defs id="ccdefs4"><style id="ccstyle2">.ccfil2{fill:#020202}.ccfil0{fill:#e3e3f8}.ccfil5{fill:#efefef}.ccfil6{fill:#faf1f1}.ccfil3{fill:#fdf2f2}.ccfil1{fill:#fdfdfd}.ccfil4{fill:#fef3f3}</style></defs><g id="ccLayer_x0020_1" transform="matrix(.8945 0 0 .8945 138.649 275.985)"><g id="cc_631799120"><g id="ccg11"><path class="ccfil0" d="M967 1895c46-30 84-105 61-158-63 27-60 89-61 158z" id="ccpath7" fill="#e3e3f8"/><path class="ccfil0" d="M1679 2067c50-16 98-72 71-130-39 27-64 64-71 130z" id="ccpath9" fill="#e3e3f8"/></g><g id="ccg21"><path class="ccfil1" d="M280 2895c0 63 16 131 60 155 162 91 730 20 923-23 101-23 183-98 278-139 214-93 369-168 540-293 124-91 321-347 342-500l-169-38c-4 172-43 211-196 251-103 28-304 34-409 16-139-23-202-96-265-179-122-162 27-275-166-286-203 249-561 70-718 45-67 97-224 727-222 871 97-33 158 3 245 37 308 119 39 224-84 193-84-20-110-75-159-110z" id="ccpath13" fill="#fdfdfd"/><path class="ccfil1" d="M683 1458c125 24 236 76 342 129 173 86 204 74 220 194 2 22-2 34 61 54 106 33-61-26 223-25 169 1 556 69 681 148 52 33 42 75 218 70-2-207-57-516-138-706-99-230-230-265-497-351-156-50-614-105-756-17-133 83-158 182-282 356-36 51-49 90-72 148z" id="ccpath15" fill="#fdfdfd"/><path class="ccfil1" d="M1784 1883c100 41-5 306-144 242-45-127 62-199 91-256-60-9-231-36-282-17-66 25-81 166-47 232 160 314 867 247 792 3-30-99-58-115-159-149-81-27-162-55-251-55z" id="ccpath17" fill="#fdfdfd"/><path class="ccfil1" d="M527 1848c80 77 261 89 378 95 15-155 28-271 152-262 61 83 29 181-35 244 109-1 172-83 156-202-92-66-371-198-511-217-39 42-135 272-140 342z" id="ccpath19" fill="#fdfdfd"/></g><path class="ccfil2" d="M339 2838c66-6 238 44 252 100-107 13-243 3-252-100zm-59 57c49 35 75 90 159 110 123 31 392-74 84-193-87-34-148-70-245-37-2-144 155-774 222-871 157 25 515 204 718-45 193 11 44 124 166 286 63 83 126 156 265 179 105 18 306 12 409-16 153-40 192-79 196-251l169 38c-21 153-218 409-342 500-171 125-326 200-540 293-95 41-177 116-278 139-193 43-761 114-923 23-44-24-60-92-60-155zm1399-828c7-66 32-103 71-130 27 58-21 114-71 130zm105-184c89 0 170 28 251 55 101 34 129 50 159 149 75 244-632 311-792-3-34-66-19-207 47-232 51-19 222 8 282 17-29 57-136 129-91 256 139 64 244-201 144-242zm-817 12c1-69-2-131 61-158 23 53-15 128-61 158zm-440-47c5-70 101-300 140-342 140 19 419 151 511 217 16 119-47 201-156 202 64-63 96-161 35-244-124-9-137 107-152 262-117-6-298-18-378-95zm-100-80c-37-102-37-261 120-274l-80 223c-21 48-21 37-40 51zm256-310c23-58 36-97 72-148 124-174 149-273 282-356 142-88 600-33 756 17 267 86 398 121 497 351 81 190 136 499 138 706-176 5-166-37-218-70-125-79-512-147-681-148-284-1-117 58-223 25-63-20-59-32-61-54-16-120-47-108-220-194-106-53-217-105-342-129zm1770-49c-19-63 16-59 77-102 35-25 63-51 106-75 161-90 461-105 589 2 52 43 137 127 124 237-27 219-177 339-300 439-125 102-333 207-548 137-18-44-4-323-25-426-19-92-9-102 44-157 156-162 494-280 686-141 81 60 58 83 100 129 52-56-45-244-403-232-243 8-348 198-450 189zM997 840c5-139 133-427 261-527 155-120 317-233 555-98 59 33 56 50 62 132 5 79-2 108-22 172-158 510-290 217-796 338 19-166 163-314 243-391 137-133 236-219 442-191 57 95 63 155-6 266-92 148-115 139-101 240 72-18 94-88 127-158 201-420-91-471-270-394-120 51-334 287-404 429-14 28-29 64-42 95zm792 21c21-125 145-156 145-541 0-166-204-315-471-204-229 94-264 166-386 350-115 174-111 365-210 526-29 46-55 62-87 108-23 34-40 77-63 117-47 77-95 133-133 225-120 3-221 5-233 129-16 170 64 212 64 276-1 69-281 765-203 1180 22 114 97 115 217 129 289 35 664 23 923-81l470-225c119-67 319-194 408-287 63-65 96-120 150-197 74-108 76-106 92-253 98 18 281 61 342 114-7 69-41 36-41 98 39 1 104-48 120-102-41-60-84-50-143-98 47-37 132-54 197-81 140-58 379-234 438-394 47-129 12-344-64-428-80-88-266-133-418-133-181 0-368 130-514 186-56-49-60-105-101-159-47-64-353-224-499-255z" id="ccpath23" fill="#020202"/><path class="ccfil3" d="M2453 1409c102 9 207-181 450-189 358-12 455 176 403 232-42-46-19-69-100-129-192-139-530-21-686 141-53 55-63 65-44 157 21 103 7 382 25 426 215 70 423-35 548-137 123-100 273-220 300-439 13-110-72-194-124-237-128-107-428-92-589-2-43 24-71 50-106 75-61 43-96 39-77 102z" id="ccpath25" fill="#fdf2f2"/><path class="ccfil4" d="M997 840l49-87c13-31 28-67 42-95 70-142 284-378 404-429 179-77 471-26 270 394-33 70-55 140-127 158-14-101 9-92 101-240 69-111 63-171 6-266-206-28-305 58-442 191-80 77-224 225-243 391 506-121 638 172 796-338 20-64 27-93 22-172-6-82-3-99-62-132-238-135-400-22-555 98-128 100-256 388-261 527z" id="ccpath27" fill="#fef3f3"/><path class="ccfil5" d="M427 1768c19-14 19-3 40-51l80-223c-157 13-157 172-120 274z" id="ccpath29" fill="#efefef"/><path class="ccfil6" d="M591 2938c-14-56-186-106-252-100 9 103 145 113 252 100z" id="ccpath31" fill="#faf1f1"/></g></g></symbol><symbol viewBox="0 0 24 24" id="elixir" xmlns="http://www.w3.org/2000/svg"><path d="M12.431 22.383c-3.86 0-6.99-3.64-6.99-8.13 0-3.678 2.774-8.172 4.916-10.91 1.014-1.295 2.931-2.321 2.931-2.321s-.982 5.238 1.683 7.318c2.365 1.847 4.105 4.25 4.105 6.363 0 4.232-2.784 7.68-6.645 7.68z" fill="#9575cd" stroke-width="1.256"/></symbol><symbol viewBox="0 0 323.00001 322.99999" id="elm" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.8053 0 0 .8053 30.106 31.524)"><path fill="#f0ad00" d="M160.8 153.865l68.028-68.03H92.77z"/><path fill="#7fd13b" d="M160.983 5.098H12.033l68.524 68.525H229.51z"/><path fill="#7fd13b" stroke-width=".974" d="M243.906 88.021l74.136 74.137-74.474 74.475-74.137-74.137z"/><path fill="#60b5cc" d="M318.2 145.045V5.098H178.252z"/><path fill="#5a6378" d="M152.164 162.499L3.4 13.733v297.533z"/><path fill="#f0ad00" d="M252.205 245.27l65.995 65.996v-131.99z"/><path fill="#60b5cc" d="M160.8 171.134L12.034 319.899h297.53z"/></g></symbol><symbol viewBox="0 0 24 24" id="email" xmlns="http://www.w3.org/2000/svg"><path d="M20 8l-8 5-8-5V6l8 5 8-5m0-2H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 30 30" id="erlang" xmlns="http://www.w3.org/2000/svg"><path style="line-height:1.25;-inkscape-font-specification:'Wide Latin'" d="M5.217 4.367c-.048.052-.097.1-.144.153C2.697 7.182 1.51 10.798 1.51 15.366c0 4.418 1.156 7.862 3.46 10.34h19.414c2.553-1.152 4.127-3.43 4.127-3.43l-3.147-2.52-1.454 1.381c-.866.773-.845.931-2.314 1.78-1.496.674-3.04.966-4.634.966-2.516 0-4.423-.909-5.723-2.059-1.286-1.15-1.985-4.511-2.097-6.68l17.458.067-.182-1.472s-.847-7.129-2.542-9.372zm8.76.846c1.565 0 3.22.535 3.96 1.471.742.937.932 1.667.974 3.524H9.12c.111-1.955.436-2.81 1.372-3.697.937-.888 2.03-1.298 3.484-1.298z" font-weight="400" font-size="48" font-family="Wide Latin" letter-spacing="0" word-spacing="0" fill="#f44336" stroke-width=".97"/></symbol><symbol viewBox="0 0 299.99999 300.00001" id="eslint" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-2.88 18.438) scale(1.0344)"><path d="M97.021 99.016l48.432-27.962c1.212-.7 2.706-.7 3.918 0l48.433 27.962a3.92 3.92 0 0 1 1.959 3.393v55.924a3.924 3.924 0 0 1-1.959 3.394l-48.433 27.962c-1.212.7-2.706.7-3.918 0l-48.432-27.962a3.92 3.92 0 0 1-1.959-3.394v-55.924a3.922 3.922 0 0 1 1.959-3.393" fill="#7986cb"/><path d="M273.34 124.49L215.473 23.82c-2.102-3.64-5.985-6.325-10.188-6.325H89.545c-4.204 0-8.088 2.685-10.19 6.325L21.488 124.27c-2.102 3.641-2.102 8.236 0 11.877l57.867 99.847c2.102 3.64 5.986 5.501 10.19 5.501h115.74c4.203 0 8.087-1.805 10.188-5.446l57.867-100.01c2.104-3.639 2.104-7.907.001-11.547m-47.917 48.41c0 1.48-.891 2.849-2.174 3.59l-73.71 42.527a4.194 4.194 0 0 1-4.17 0l-73.767-42.527c-1.282-.741-2.179-2.109-2.179-3.59V87.847c0-1.481.884-2.849 2.167-3.59l73.707-42.527a4.185 4.185 0 0 1 4.168 0l73.772 42.527c1.283.741 2.186 2.109 2.186 3.59z" fill="#3f51b5"/></g></symbol><symbol viewBox="0 0 24 24" id="exe" xmlns="http://www.w3.org/2000/svg"><path d="M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h14m0 14V8H5v10h14z" fill="#e64a19"/></symbol><symbol viewBox="0 0 24 24" id="favicon" xmlns="http://www.w3.org/2000/svg"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.62L12 2 9.19 8.62 2 9.24l5.45 4.73L5.82 21 12 17.27z" fill="#ffd54f"/></symbol><symbol viewBox="0 0 24 24" id="file" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m5 2H6v16h12v-9h-7V4z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 400 400" id="firebase" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 103)"><path d="M72.55 208.77l44.456-292.29 56.209 90.445L195.49-37.57 330.6 209.28z" fill="#ffa712"/><path d="M195.7 276.73l134.9-67.45-46.5-224.83L72.55 208.77z" fill="#fcca3f"/><path d="M173.22 6.932L72.56 208.772l136.35-144.58z" fill="#f6820c"/></g></symbol><symbol viewBox="0 0 24 24" id="flash" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="cma"><stop offset="0" stop-color="#d92f3c"/><stop offset="1" stop-color="#791223"/></linearGradient><linearGradient xlink:href="#cma" id="cmb" x1="2.373" y1="12.027" x2="21.86" y2="12.027" gradientUnits="userSpaceOnUse" gradientTransform="translate(-.09 -24.144)"/></defs><rect width="19.487" height="19.487" x="2.283" y="-21.86" transform="rotate(90)" ry="0" fill="url(#cmb)"/><path style="line-height:125%" d="M16.802 5.768l-.013.002a6.43 6.43 0 0 0-1.182.192 5.062 5.062 0 0 0-1.494.718c-.428.323-.817.72-1.17 1.191-.34.48-.682 1.032-1.022 1.66-.12.228-.233.424-.35.636v.002h-.004l-1.34 2.394-.005-.002c-.238.443-.461.847-.665 1.198a4.358 4.358 0 0 1-.716.94 2.79 2.79 0 0 1-.907.594c-.072.027-.161.042-.242.063h-.989v2.414h.989v-.002a6.427 6.427 0 0 0 1.185-.192 5.062 5.062 0 0 0 1.494-.718 5.94 5.94 0 0 0 1.171-1.191c.34-.48.681-1.033 1.021-1.66.12-.228.235-.425.353-.637l.006.002.003-.005.037-.066h2.53v.002h1.124v-2.408h-.33v-.001h-1.98c.22-.407.432-.789.621-1.115.214-.37.452-.682.717-.94a2.79 2.79 0 0 1 .906-.594c.07-.027.16-.041.239-.061h.992V8.18h-.002V5.77h-.977v-.002z" font-weight="400" font-size="40" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#fff"/></symbol><symbol class="cnflow-logo" viewBox="0 0 299.99999 300" id="flow" xmlns="http://www.w3.org/2000/svg"><title>Flow logo</title><path d="M38.75 33.427l77.461 77.47H54.436l61.145 61.16H38.437l93.462 93.478v-77.158l.01-.01v-77.47h-.01V66.982h46.691l20.394 20.393H153.57v76.531h22.05l24.474 24.473h-15.806l-.01-.01v.01h-31.665l-.01-.01v.01h-.313l.313.313v77.148h109.149l-39.2-39.2v-15.806l8.465 8.466v-77.37h-15.682l.017-38.191 30.09 30.086V56.362h-64.874l-22.94-22.934H113.71z" fill="#fbc02d" fill-opacity=".976" stroke-width=".955" class="cnflow-logo-mark"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-aurelia" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="coa" x1="-388.15%" x2="237.68%" y1="-144.18%" y2="430.41%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cob" x1="72.945%" x2="-97.052%" y1="84.424%" y2="-147.7%"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="coc" x1="-283.88%" x2="287.54%" y1="-693.6%" y2="101.71%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cod" x1="-821.19%" x2="101.99%" y1="-469.05%" y2="288.24%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="coe" x1="-140.36%" x2="419.01%" y1="-230.93%" y2="261.98%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cof" x1="191.08%" x2="20.358%" y1="253.95%" y2="20.403%"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="cog" x1="-388.09%" x2="237.67%" y1="-173.85%" y2="518.99%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="coi" x1="-31.824" x2="19.682" y1="-11.741" y2="35.548" gradientTransform="scale(.95818 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#coa"/><linearGradient id="coj" x1="12.022" x2="-15.716" y1="13.922" y2="-23.952" gradientTransform="scale(.96226 1.0392)" gradientUnits="userSpaceOnUse" xlink:href="#cob"/><linearGradient id="cok" x1="-23.39" x2="23.931" y1="-57.289" y2="8.573" gradientTransform="scale(1.0429 .95884)" gradientUnits="userSpaceOnUse" xlink:href="#coc"/><linearGradient id="col" x1="-53.331" x2="6.771" y1="-30.517" y2="18.785" gradientTransform="scale(.99898 1.001)" gradientUnits="userSpaceOnUse" xlink:href="#cod"/><linearGradient id="com" x1="-14.029" x2="41.998" y1="-23.111" y2="26.259" gradientTransform="scale(1.0003 .99965)" gradientUnits="userSpaceOnUse" xlink:href="#coe"/><linearGradient id="con" x1="31.177" x2="3.37" y1="41.442" y2="3.402" gradientTransform="scale(.96254 1.0389)" gradientUnits="userSpaceOnUse" xlink:href="#cof"/><linearGradient id="coo" x1="-31.905" x2="19.599" y1="-14.258" y2="42.767" gradientTransform="scale(.95823 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#cog"/><linearGradient id="cop" x1="4.301" x2="34.534" y1="34.41" y2="4.514" gradientTransform="scale(1.002 .99796)" gradientUnits="userSpaceOnUse" xlink:href="#coh"/><linearGradient id="coh" x1=".112" x2=".901" y1=".897" y2=".116"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".53"/><stop stop-color="#CD0F7E" offset=".79"/><stop stop-color="#ED2C89" offset="1"/></linearGradient></defs><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#f06292" fill-rule="nonzero"/><g transform="matrix(.31022 .0619 -.0619 .31022 11.807 7.546)" fill="none"><path d="M8.002 6.127L4.117 8.719.116 2.723 4 .13z" transform="rotate(-11.284 17.839 -78.732)" fill="url(#coi)"/><path d="M9.179 1.887l6.637 9.946-7.906 5.276-6.637-9.946L.115 5.43 8.02.153z" transform="rotate(-11.284 129.49 -99.884)" fill="url(#coj)"/><path d="M7.3 1.88l1.462 2.189-6.018 4.015L.124 4.16l1.315-.877L6.143.144z" transform="rotate(-11.284 167.2 -62.32)" fill="url(#cok)"/><path d="M2.328 1.146L4.016.02l2.619 3.925L2.75 6.537 1.29 4.347l2.197-1.466zm-1.04 3.201L.132 2.612l2.197-1.466 1.158 1.735z" transform="rotate(-11.284 104.37 -149.22)" fill="url(#col)"/><path d="M5.346 9.155l-1.315.877L.03 4.035 6.047.019l2.805 4.204L4.15 7.36l4.703-3.138 1.197 1.793z" transform="rotate(-11.284 81.819 7.645)" fill="url(#com)"/><path d="M14.533 9.934l1.197 1.793-7.907 5.276-1.196-1.793L.052 5.358 7.958.082z" transform="rotate(-11.284 17.141 -7.825)" fill="url(#con)"/><path d="M6.235 7.177L4.038 8.643 2.84 6.849.036 2.646 3.92.053 7.923 6.05z" transform="rotate(-11.284 18.188 -79.174)" fill="url(#coo)"/><path d="M18.955 35.925L17.48 34.45l3.998-3.998 1.475 1.475z" fill="#714896"/><path d="M33.33 21.55l-1.475-1.474 1.867-1.868 1.475 1.475z" fill="#6f4795"/><path d="M7.12 24.09l-1.525-1.525 3.998-3.998 1.525 1.525z" fill="#88519f"/><path d="M21.495 9.714L19.97 8.19l1.868-1.868 1.524 1.525z" fill="#85509e"/><path d="M31.418 23.462l-6.72 6.72-1.475-1.474 6.72-6.721z" fill="#8d166a"/><path d="M18.058 10.101l1.525 1.525-6.721 6.72-1.525-1.524z" fill="#a70d6f"/><path d="M2.375 11.769l1.9 1.9-1.9 1.901-1.901-1.9z" fill="#9e61ad"/><path d="M15.523 36.482l1.9 1.9-1.9 1.901-1.9-1.9z" fill="#8053a3"/><path d="M8.372 38.294L.017 29.876 29.749.08l8.636 8.201z" transform="translate(1.823 1.548)" fill="url(#cop)"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-aurelia-open" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="cpi" x1="-31.824" x2="19.682" y1="-11.741" y2="35.548" gradientTransform="scale(.95818 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#cpa"/><linearGradient id="cpa" x1="-3.881" x2="2.377" y1="-1.442" y2="4.304"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpj" x1="12.022" x2="-15.716" y1="13.922" y2="-23.952" gradientTransform="scale(.96226 1.0392)" gradientUnits="userSpaceOnUse" xlink:href="#cpb"/><linearGradient id="cpb" x1=".729" x2="-.971" y1=".844" y2="-1.477"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="cpk" x1="-23.39" x2="23.931" y1="-57.289" y2="8.573" gradientTransform="scale(1.0429 .95884)" gradientUnits="userSpaceOnUse" xlink:href="#cpc"/><linearGradient id="cpc" x1="-2.839" x2="2.875" y1="-6.936" y2="1.017"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpl" x1="-53.331" x2="6.771" y1="-30.517" y2="18.785" gradientTransform="scale(.99898 1.001)" gradientUnits="userSpaceOnUse" xlink:href="#cpd"/><linearGradient id="cpd" x1="-8.212" x2="1.02" y1="-4.691" y2="2.882"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpm" x1="-14.029" x2="41.998" y1="-23.111" y2="26.259" gradientTransform="scale(1.0003 .99965)" gradientUnits="userSpaceOnUse" xlink:href="#cpe"/><linearGradient id="cpe" x1="-1.404" x2="4.19" y1="-2.309" y2="2.62"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpn" x1="31.177" x2="3.37" y1="41.442" y2="3.402" gradientTransform="scale(.96254 1.0389)" gradientUnits="userSpaceOnUse" xlink:href="#cpf"/><linearGradient id="cpf" x1="1.911" x2=".204" y1="2.539" y2=".204"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="cpo" x1="-31.905" x2="19.599" y1="-14.258" y2="42.767" gradientTransform="scale(.95823 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#cpg"/><linearGradient id="cpg" x1="-3.881" x2="2.377" y1="-1.738" y2="5.19"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpp" x1="4.301" x2="34.534" y1="34.41" y2="4.514" gradientTransform="scale(1.002 .99796)" gradientUnits="userSpaceOnUse" xlink:href="#cph"/><linearGradient id="cph" x1=".112" x2=".901" y1=".897" y2=".116"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".53"/><stop stop-color="#CD0F7E" offset=".79"/><stop stop-color="#ED2C89" offset="1"/></linearGradient></defs><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#f06292" fill-rule="nonzero"/><g transform="matrix(.31022 .0619 -.0619 .31022 11.807 7.546)" fill="none"><path d="M8.002 6.127L4.117 8.719.116 2.723 4 .13z" transform="rotate(-11.284 17.839 -78.732)" fill="url(#cpi)"/><path d="M9.179 1.887l6.637 9.946-7.906 5.276-6.637-9.946L.115 5.43 8.02.153z" transform="rotate(-11.284 129.49 -99.884)" fill="url(#cpj)"/><path d="M7.3 1.88l1.462 2.189-6.018 4.015L.124 4.16l1.315-.877L6.143.144z" transform="rotate(-11.284 167.2 -62.32)" fill="url(#cpk)"/><path d="M2.328 1.146L4.016.02l2.619 3.925L2.75 6.537 1.29 4.347l2.197-1.466zm-1.04 3.201L.132 2.612l2.197-1.466 1.158 1.735z" transform="rotate(-11.284 104.37 -149.22)" fill="url(#cpl)"/><path d="M5.346 9.155l-1.315.877L.03 4.035 6.047.019l2.805 4.204L4.15 7.36l4.703-3.138 1.197 1.793z" transform="rotate(-11.284 81.819 7.645)" fill="url(#cpm)"/><path d="M14.533 9.934l1.197 1.793-7.907 5.276-1.196-1.793L.052 5.358 7.958.082z" transform="rotate(-11.284 17.141 -7.825)" fill="url(#cpn)"/><path d="M6.235 7.177L4.038 8.643 2.84 6.849.036 2.646 3.92.053 7.923 6.05z" transform="rotate(-11.284 18.188 -79.174)" fill="url(#cpo)"/><path d="M18.955 35.925L17.48 34.45l3.998-3.998 1.475 1.475z" fill="#714896"/><path d="M33.33 21.55l-1.475-1.474 1.867-1.868 1.475 1.475z" fill="#6f4795"/><path d="M7.12 24.09l-1.525-1.525 3.998-3.998 1.525 1.525z" fill="#88519f"/><path d="M21.495 9.714L19.97 8.19l1.868-1.868 1.524 1.525z" fill="#85509e"/><path d="M31.418 23.462l-6.72 6.72-1.475-1.474 6.72-6.721z" fill="#8d166a"/><path d="M18.058 10.101l1.525 1.525-6.721 6.72-1.525-1.524z" fill="#a70d6f"/><path d="M2.375 11.769l1.9 1.9-1.9 1.901-1.901-1.9z" fill="#9e61ad"/><path d="M15.523 36.482l1.9 1.9-1.9 1.901-1.9-1.9z" fill="#8053a3"/><path d="M8.372 38.294L.017 29.876 29.749.08l8.636 8.201z" transform="translate(1.823 1.548)" fill="url(#cpp)"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-components" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#cddc39" fill-rule="nonzero"/><path d="M11.185 9.613h5.346v2.9l3.782-3.775 3.775 3.775-3.775 3.782h2.9v5.346h-5.346v-5.346h2.446l-3.782-3.782v2.446h-5.346V9.613m0 6.682h5.346v5.346h-5.346z" fill="#f0f4c3" stroke-width=".668"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-components-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#cddc39"/><path d="M11.185 9.613h5.346v2.9l3.782-3.775 3.775 3.775-3.775 3.782h2.9v5.346h-5.346v-5.346h2.446l-3.782-3.782v2.446h-5.346V9.613m0 6.682h5.346v5.346h-5.346z" fill="#f0f4c3" stroke-width=".668"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-config" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#00acc1" fill-rule="nonzero"/><path d="M17.293 17.786a2.308 2.308 0 0 1-2.308-2.308 2.308 2.308 0 0 1 2.308-2.307 2.308 2.308 0 0 1 2.308 2.307 2.308 2.308 0 0 1-2.308 2.308m4.899-1.668c.026-.211.046-.422.046-.64 0-.217-.02-.435-.046-.659l1.391-1.075a.333.333 0 0 0 .08-.422l-1.32-2.28c-.079-.146-.257-.205-.402-.146l-1.641.66a4.779 4.779 0 0 0-1.115-.647l-.244-1.747a.333.333 0 0 0-.33-.277h-2.637a.333.333 0 0 0-.33.277l-.243 1.747a4.78 4.78 0 0 0-1.114.646l-1.642-.659a.324.324 0 0 0-.402.145l-1.319 2.281a.325.325 0 0 0 .08.422l1.39 1.075c-.026.224-.046.442-.046.66s.02.428.046.639l-1.39 1.094a.325.325 0 0 0-.08.422l1.319 2.282c.079.145.257.197.402.145l1.642-.666c.342.264.698.488 1.114.653l.244 1.747a.333.333 0 0 0 .33.277h2.637a.333.333 0 0 0 .33-.277l.243-1.747a4.802 4.802 0 0 0 1.115-.653l1.641.666c.145.052.323 0 .403-.145l1.318-2.282a.333.333 0 0 0-.079-.422z" fill="#80deea" stroke-width=".659"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-config-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#00acc1"/><path d="M17.293 17.786a2.308 2.308 0 0 1-2.308-2.308 2.308 2.308 0 0 1 2.308-2.307 2.308 2.308 0 0 1 2.308 2.307 2.308 2.308 0 0 1-2.308 2.308m4.899-1.668c.026-.211.046-.422.046-.64 0-.217-.02-.435-.046-.659l1.391-1.075a.333.333 0 0 0 .08-.422l-1.32-2.28c-.079-.146-.257-.205-.402-.146l-1.641.66a4.779 4.779 0 0 0-1.115-.647l-.244-1.747a.333.333 0 0 0-.33-.277h-2.637a.333.333 0 0 0-.33.277l-.243 1.747a4.78 4.78 0 0 0-1.114.646l-1.642-.659a.324.324 0 0 0-.402.145l-1.319 2.281a.325.325 0 0 0 .08.422l1.39 1.075c-.026.224-.046.442-.046.66s.02.428.046.639l-1.39 1.094a.325.325 0 0 0-.08.422l1.319 2.282c.079.145.257.197.402.145l1.642-.666c.342.264.698.488 1.114.653l.244 1.747a.333.333 0 0 0 .33.277h2.637a.333.333 0 0 0 .33-.277l.243-1.747a4.802 4.802 0 0 0 1.115-.653l1.641.666c.145.052.323 0 .403-.145l1.318-2.282a.333.333 0 0 0-.079-.422z" fill="#80deea" stroke-width=".659"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-css" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#42a5f5" fill-rule="nonzero"/><path d="M12.488 9.415l-.44 2.259h9.188l-.298 1.46h-9.18l-.447 2.251H20.5l-.514 2.576-3.705 1.224-3.211-1.224.223-1.109h-2.258l-.534 2.704 5.307 2.029 6.118-2.029.812-4.076.162-.818 1.041-5.247H12.488z" fill-rule="nonzero" fill="#bbdefb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-css-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#42a5f5" fill-rule="nonzero"/><path d="M12.488 9.415l-.44 2.259h9.188l-.298 1.46h-9.18l-.447 2.251H20.5l-.514 2.576-3.705 1.224-3.211-1.224.223-1.109h-2.258l-.534 2.704 5.307 2.029 6.118-2.029.812-4.076.162-.818 1.041-5.247H12.488z" fill-rule="nonzero" fill="#bbdefb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-dist" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#e57373" fill-rule="nonzero"/><path d="M18.575 11.113h-2.576V9.825h2.576m3.864 1.288h-2.576V9.825l-1.288-1.288h-2.576L14.71 9.825v1.288h-2.577c-.715 0-1.288.573-1.288 1.288v7.085a1.288 1.288 0 0 0 1.288 1.288H22.44a1.288 1.288 0 0 0 1.288-1.288V12.4c0-.715-.58-1.288-1.288-1.288z" fill="#ffcdd2" stroke-width=".644"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-dist-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#e57373"/><path d="M18.575 11.113h-2.576V9.825h2.576m3.864 1.288h-2.576V9.825l-1.288-1.288h-2.576L14.71 9.825v1.288h-2.577c-.715 0-1.288.573-1.288 1.288v7.085a1.288 1.288 0 0 0 1.288 1.288H22.44a1.288 1.288 0 0 0 1.288-1.288V12.4c0-.715-.58-1.288-1.288-1.288z" fill="#ffcdd2" fill-rule="evenodd" stroke-width=".644"/></symbol><symbol id="folder-docker" clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><defs id="cydefs10"><path id="cySVGID_2_" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2 2.1.9 2.1 2-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></defs><path id="cypath2" d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#039be5" fill-rule="nonzero"/><style id="cystyle2">.cyst0{fill:#fff}.cyst1{clip-path:url(#cySVGID_4_)}</style><g id="cyg34" transform="translate(8.319 9.626) scale(.39491)" fill="#b3e5fc"><g id="cyg32"><g id="cyg30"><title id="cytitle4">Group 3</title><g id="cyg28"><g id="cyg26"><g id="cyg9"><path id="cySVGID_1_" class="cyst0" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2 2.1.9 2.1 2-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></g><g id="cyg24"><clipPath id="cySVGID_4_"><use id="cyuse14" width="100%" height="100%" xlink:href="#cySVGID_2_"/></clipPath><g id="cyg22" class="cyst1" clip-path="url(#cySVGID_4_)"><g id="cyg20"><g id="cyg18"><path id="cySVGID_3_" class="cyst0" d="M-48.8-21H1226v151.4H-48.8z"/></g></g></g></g></g></g></g></g></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-docker-open" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="cza"><use width="100%" height="100%" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#SVGID_2_"/></clipPath></defs><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#039be5"/><g transform="matrix(.3949 0 0 .39489 8.319 9.626)" fill="#b3e5fc"><title>Group 3</title><path class="czst0" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2 2.1.9 2.1 2-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/><g class="czst1" clip-path="url(#cza)"><path class="czst0" d="M-48.8-21H1226v151.4H-48.8z"/></g></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-docs" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#0277bd" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m6.075 10.8v-1.35H13.85v1.35h6.075m2.025-2.7v-1.35h-8.1v1.35h8.1z" fill-rule="nonzero" fill="#b3e5fc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-docs-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#0277bd" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m6.075 10.8v-1.35H13.85v1.35h6.075m2.025-2.7v-1.35h-8.1v1.35h8.1z" fill-rule="nonzero" fill="#b3e5fc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-expo" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#01579b" fill-rule="nonzero"/><style>.dcst0{fill:#1173b6}.st1{fill:#585d67}</style><path class="dcst0" d="M18.575 9.82c-.489-.745-.605-.844-1.6-.844h-.024c-.996 0-1.106.099-1.601.844-.46.699-5.024 9.058-5.024 9.291 0 .338.087.658.402 1.112.32.46.873.716 1.275.309.273-.274 3.201-5.321 4.616-7.23a.425.425 0 0 1 .693 0c1.414 1.909 4.343 6.956 4.616 7.23.402.407.955.15 1.275-.309.314-.454.402-.774.402-1.112-.006-.233-4.57-8.598-5.03-9.291z" fill="#1173b6" stroke-width=".058"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-expo-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#01579b"/><path class="ddst0" d="M18.575 9.82c-.489-.745-.605-.844-1.6-.844h-.024c-.996 0-1.106.099-1.601.844-.46.699-5.024 9.058-5.024 9.291 0 .338.087.658.402 1.112.32.46.873.716 1.275.309.273-.274 3.201-5.321 4.616-7.23a.425.425 0 0 1 .693 0c1.414 1.909 4.343 6.956 4.616 7.23.402.407.955.15 1.275-.309.314-.454.402-.774.402-1.112-.006-.233-4.57-8.598-5.03-9.291z" fill="#1173b6" stroke-width=".058" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-font" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ef9a9a" fill-rule="nonzero"/><path d="M14.62 17.403l2.38-6.33 2.37 6.33m-3.37-9l-5.5 14h2.25l1.12-3h6.25l1.13 3h2.25l-5.5-14h-2z" fill="#f44336" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-font-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ef9a9a" fill-rule="nonzero"/><path d="M14.62 17.403l2.38-6.33 2.37 6.33m-3.37-9l-5.5 14h2.25l1.12-3h6.25l1.13 3h2.25l-5.5-14h-2z" fill="#f44336" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-git" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ff8a65" fill-rule="nonzero"/><path d="M10.43 14.14l4.044-4.052 1.183 1.19a1.387 1.387 0 0 0 .65 1.56v3.877c-.42.238-.699.693-.699 1.21 0 .768.632 1.4 1.4 1.4.767 0 1.4-.632 1.4-1.4 0-.517-.28-.972-.7-1.21v-3.4l1.448 1.462c-.05.105-.05.224-.05.35 0 .767.633 1.399 1.4 1.399.768 0 1.4-.632 1.4-1.4 0-.767-.632-1.4-1.4-1.4-.126 0-.245 0-.35.05l-1.798-1.799a1.385 1.385 0 0 0-.805-1.637c-.3-.112-.615-.14-.895-.063l-1.19-1.183.553-.545a1.381 1.381 0 0 1 1.973 0l5.591 5.59a1.381 1.381 0 0 1 0 1.974l-5.59 5.591a1.381 1.381 0 0 1-1.974 0l-5.591-5.59a1.381 1.381 0 0 1 0-1.974z" fill="#e64a19" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-git-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ff8a65" fill-rule="nonzero"/><path d="M10.43 14.14l4.044-4.052 1.183 1.19a1.387 1.387 0 0 0 .65 1.56v3.877c-.42.238-.699.693-.699 1.21 0 .768.632 1.4 1.4 1.4.767 0 1.4-.632 1.4-1.4 0-.517-.28-.972-.7-1.21v-3.4l1.448 1.462c-.05.105-.05.224-.05.35 0 .767.633 1.399 1.4 1.399.768 0 1.4-.632 1.4-1.4 0-.767-.632-1.4-1.4-1.4-.126 0-.245 0-.35.05l-1.798-1.799a1.385 1.385 0 0 0-.805-1.637c-.3-.112-.615-.14-.895-.063l-1.19-1.183.553-.545a1.381 1.381 0 0 1 1.973 0l5.591 5.59a1.381 1.381 0 0 1 0 1.974l-5.59 5.591a1.381 1.381 0 0 1-1.974 0l-5.591-5.59a1.381 1.381 0 0 1 0-1.974z" fill="#e64a19" fill-rule="nonzero"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-global" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#5c6bc0" fill-rule="nonzero"/><path d="M21.132 18.585a1.22 1.22 0 0 0-1.156-.846h-.609v-1.825a.608.608 0 0 0-.608-.609h-3.65v-1.217h1.216a.608.608 0 0 0 .609-.608v-1.217h1.217a1.217 1.217 0 0 0 1.216-1.217v-.25a4.858 4.858 0 0 1 1.765 7.79m-4.198 1.545a4.86 4.86 0 0 1-4.26-4.826c0-.377.049-.742.128-1.089l2.915 2.915v.608a1.217 1.217 0 0 0 1.217 1.217m.608-9.735a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.085-6.085 6.085 6.085 0 0 0-6.085-6.084z" fill="#c5cae9" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-global-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#5c6bc0"/><path d="M21.133 18.585a1.22 1.22 0 0 0-1.156-.846h-.609v-1.825a.608.608 0 0 0-.608-.609h-3.65v-1.217h1.216a.608.608 0 0 0 .609-.608v-1.217h1.217a1.217 1.217 0 0 0 1.216-1.217v-.25a4.858 4.858 0 0 1 1.765 7.79m-4.198 1.545a4.86 4.86 0 0 1-4.26-4.826c0-.377.049-.742.128-1.089l2.915 2.915v.608a1.217 1.217 0 0 0 1.216 1.217m.609-9.735a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.085-6.085 6.085 6.085 0 0 0-6.085-6.084z" fill="#c5cae9" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-i18n" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#5c6bc0" fill-rule="nonzero"/><path d="M17.293 17.786l-1.53-1.512.018-.018a10.555 10.555 0 0 0 2.235-3.934h1.765v-1.205h-4.217V9.912h-1.205v1.205h-4.217v1.205h6.73a9.5 9.5 0 0 1-1.91 3.223 9.424 9.424 0 0 1-1.392-2.018h-1.205c.44.982 1.042 1.91 1.795 2.747l-3.067 3.024.856.856 3.012-3.013 1.874 1.874.458-1.229m3.392-3.054H19.48l-2.711 7.23h1.205l.674-1.808h2.862l.68 1.807h1.206l-2.711-7.23m-1.579 4.218l.976-2.609.976 2.609z" fill="#c5cae9" stroke-width=".602"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-i18n-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#5c6bc0"/><path d="M17.293 17.786l-1.53-1.512.018-.018a10.555 10.555 0 0 0 2.235-3.934h1.765v-1.205h-4.217V9.912h-1.205v1.205h-4.217v1.205h6.73a9.5 9.5 0 0 1-1.91 3.223 9.424 9.424 0 0 1-1.392-2.018h-1.205c.44.982 1.042 1.91 1.795 2.747l-3.067 3.024.856.856 3.012-3.013 1.874 1.874.458-1.229m3.392-3.054H19.48l-2.711 7.23h1.205l.674-1.808h2.862l.68 1.807h1.206l-2.711-7.23m-1.579 4.218l.976-2.609.976 2.609z" fill="#c5cae9" stroke-width=".602"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-images" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#009688" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m0 12.15h8.1v-5.4l-2.7 2.7-1.35-1.35-4.05 4.05m1.35-7.425c-.74 0-1.35.61-1.35 1.35s.61 1.35 1.35 1.35 1.35-.61 1.35-1.35-.61-1.35-1.35-1.35z" fill-rule="nonzero" fill="#b2dfdb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-images-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#009688" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m0 12.15h8.1v-5.4l-2.7 2.7-1.35-1.35-4.05 4.05m1.35-7.425c-.74 0-1.35.61-1.35 1.35s.61 1.35 1.35 1.35 1.35-.61 1.35-1.35-.61-1.35-1.35-1.35z" fill-rule="nonzero" fill="#b2dfdb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-include" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#039be5" fill-rule="nonzero"/><path d="M20.788 15.981h-2.434v2.434h-1.217V15.98h-2.434v-1.217h2.434V12.33h1.217v2.434h2.434m-3.042-5.476a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.084-6.085 6.085 6.085 0 0 0-6.084-6.084z" fill="#b3e5fc" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-include-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#039be5"/><path d="M20.788 15.981h-2.434v2.434h-1.217V15.98h-2.434v-1.217h2.434V12.33h1.217v2.434h2.434m-3.042-5.476a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.084-6.085 6.085 6.085 0 0 0-6.084-6.084z" fill="#b3e5fc" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-javascript" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ffca28" fill-rule="nonzero"/><path d="M17.935 18.374a2.18 2.18 0 0 0 1.972 1.213c.829 0 1.354-.415 1.354-.987 0-.682-.542-.927-1.452-1.324l-.502-.216c-1.435-.613-2.404-1.378-2.404-3.005 0-1.5 1.167-2.638 2.917-2.638a2.957 2.957 0 0 1 2.842 1.599l-1.552.999a1.362 1.362 0 0 0-1.29-.858.873.873 0 0 0-.957.858c0 .583.374.84 1.226 1.213l.502.216c1.697.733 2.654 1.47 2.654 3.139 0 1.798-1.411 2.783-3.308 2.783a3.839 3.839 0 0 1-3.618-2.046zm-7.048.175c.315.583.583 1.027 1.283 1.027s1.066-.256 1.066-1.255v-6.774h1.998v6.804c0 2.064-1.214 3.01-2.982 3.01a3.104 3.104 0 0 1-2.993-1.826z" fill-rule="nonzero" fill="#ffecb3"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-javascript-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ffca28"/><path d="M17.935 18.374a2.18 2.18 0 0 0 1.972 1.213c.829 0 1.354-.415 1.354-.987 0-.682-.542-.927-1.452-1.324l-.502-.216c-1.435-.613-2.404-1.378-2.404-3.005 0-1.5 1.167-2.638 2.917-2.638a2.957 2.957 0 0 1 2.842 1.599l-1.552.999a1.362 1.362 0 0 0-1.29-.858.873.873 0 0 0-.957.858c0 .583.374.84 1.226 1.213l.502.216c1.697.733 2.654 1.47 2.654 3.139 0 1.798-1.412 2.783-3.308 2.783a3.839 3.839 0 0 1-3.618-2.046zm-7.048.175c.315.583.583 1.027 1.283 1.027s1.066-.256 1.066-1.255v-6.774h1.998v6.804c0 2.064-1.214 3.01-2.982 3.01a3.104 3.104 0 0 1-2.993-1.826z" fill="#ffecb3"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-lib" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#c0ca33" fill-rule="nonzero"/><path d="M17.39 12.544a2.05 2.05 0 0 0 2.05-2.05 2.05 2.05 0 0 0-2.05-2.052 2.05 2.05 0 0 0-2.05 2.051 2.05 2.05 0 0 0 2.05 2.051m0 2.42a8.992 8.992 0 0 0-6.152-2.42v7.52c2.392 0 4.539.923 6.152 2.42a8.992 8.992 0 0 1 6.152-2.42v-7.52c-2.392 0-4.539.923-6.152 2.42z" fill="#f0f4c3" stroke-width=".684"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-lib-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#c0ca33"/><path d="M17.391 12.543a2.05 2.05 0 0 0 2.05-2.05 2.05 2.05 0 0 0-2.05-2.052 2.05 2.05 0 0 0-2.05 2.051 2.05 2.05 0 0 0 2.05 2.051m0 2.42a8.992 8.992 0 0 0-6.152-2.42v7.52c2.392 0 4.539.923 6.152 2.42a8.992 8.992 0 0 1 6.152-2.42v-7.52c-2.392 0-4.539.923-6.152 2.42z" fill="#f0f4c3" stroke-width=".684"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-actions" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ab47bc" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#e1bee7" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-actions-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ab47bc"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#e1bee7" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-effects" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#00bcd4" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#b2ebf2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-effects-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#00bcd4"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#b2ebf2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-reducer" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ef5350" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#ffcdd2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-reducer-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ef5350"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#ffcdd2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-state" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#8bc34a" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#dcedc8" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-state-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#8bc34a"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#dcedc8" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-node" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#8bc34a" fill-rule="nonzero"/><path d="M17.25 8.403c-.188 0-.382.048-.542.139l-5.166 2.986a1.096 1.096 0 0 0-.542.944v5.959c0 .388.208.75.542.944l1.354.778c.66.32.882.326 1.187.326.973 0 1.535-.59 1.535-1.618V12.98a.154.154 0 0 0-.153-.153h-.646c-.09 0-.16.07-.16.153v5.882c0 .458-.472.91-1.228.528l-1.424-.813a.181.181 0 0 1-.076-.145v-5.959c0-.062.027-.118.076-.146l5.167-2.979a.15.15 0 0 1 .152 0l5.167 2.98a.164.164 0 0 1 .076.145v5.959a.181.181 0 0 1-.076.145l-5.167 2.98c-.041.027-.11.027-.16 0l-1.305-.792c-.055-.021-.111-.028-.146-.007-.368.208-.437.25-.778.354-.083.028-.215.076.05.222l1.721 1.021c.167.097.348.146.542.146s.375-.049.542-.146l5.166-2.979c.334-.194.542-.556.542-.944v-5.959c0-.389-.208-.75-.542-.944l-5.166-2.986a1.103 1.103 0 0 0-.542-.14m1.389 4.272c-1.472 0-2.354.618-2.354 1.66 0 1.117.875 1.444 2.291 1.583 1.688.166 1.82.416 1.82.75 0 .576-.465.82-1.549.82-1.375 0-1.666-.341-1.77-1.022a.157.157 0 0 0-.153-.125h-.667c-.083 0-.146.063-.146.153 0 .861.472 1.903 2.736 1.903 1.632 0 2.57-.646 2.57-1.771 0-1.118-.75-1.41-2.34-1.625-1.605-.208-1.765-.32-1.765-.694 0-.313.14-.73 1.327-.73 1.042 0 1.451.23 1.611.945.014.07.076.118.146.118h.673a.134.134 0 0 0 .105-.049c.027-.028.048-.07.034-.11-.097-1.237-.916-1.806-2.57-1.806z" fill-rule="nonzero" fill="#f1f8e9"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-node-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#8bc34a" fill-rule="nonzero"/><path d="M17.25 8.403c-.188 0-.382.048-.542.139l-5.166 2.986a1.096 1.096 0 0 0-.542.944v5.959c0 .388.208.75.542.944l1.354.778c.66.32.882.326 1.187.326.973 0 1.535-.59 1.535-1.618V12.98a.154.154 0 0 0-.153-.153h-.646c-.09 0-.16.07-.16.153v5.882c0 .458-.472.91-1.228.528l-1.424-.813a.181.181 0 0 1-.076-.145v-5.959c0-.062.027-.118.076-.146l5.167-2.979a.15.15 0 0 1 .152 0l5.167 2.98a.164.164 0 0 1 .076.145v5.959a.181.181 0 0 1-.076.145l-5.167 2.98c-.041.027-.11.027-.16 0l-1.305-.792c-.055-.021-.111-.028-.146-.007-.368.208-.437.25-.778.354-.083.028-.215.076.05.222l1.721 1.021c.167.097.348.146.542.146s.375-.049.542-.146l5.166-2.979c.334-.194.542-.556.542-.944v-5.959c0-.389-.208-.75-.542-.944l-5.166-2.986a1.103 1.103 0 0 0-.542-.14m1.389 4.272c-1.472 0-2.354.618-2.354 1.66 0 1.117.875 1.444 2.291 1.583 1.688.166 1.82.416 1.82.75 0 .576-.465.82-1.549.82-1.375 0-1.666-.341-1.77-1.022a.157.157 0 0 0-.153-.125h-.667c-.083 0-.146.063-.146.153 0 .861.472 1.903 2.736 1.903 1.632 0 2.57-.646 2.57-1.771 0-1.118-.75-1.41-2.34-1.625-1.605-.208-1.765-.32-1.765-.694 0-.313.14-.73 1.327-.73 1.042 0 1.451.23 1.611.945.014.07.076.118.146.118h.673a.134.134 0 0 0 .105-.049c.027-.028.048-.07.034-.11-.097-1.237-.916-1.806-2.57-1.806z" fill-rule="nonzero" fill="#f1f8e9"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-public" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#039be5" fill-rule="nonzero"/><path d="M20.036 16.746c.05-.408.087-.817.087-1.237s-.037-.83-.087-1.238h2.091c.099.396.16.81.16 1.238a5.1 5.1 0 0 1-.16 1.237m-3.186 3.44c.371-.687.656-1.43.854-2.203h1.825a4.968 4.968 0 0 1-2.679 2.203m-.155-3.44h-2.895c-.062-.408-.099-.817-.099-1.237s.037-.835.1-1.238h2.894c.056.403.1.817.1 1.238s-.044.829-.1 1.237m-1.447 3.687a8.39 8.39 0 0 1-1.182-2.45h2.363a8.39 8.39 0 0 1-1.181 2.45m-2.475-7.399h-1.806a4.902 4.902 0 0 1 2.672-2.202c-.37.686-.65 1.429-.866 2.202m-1.806 4.95h1.806c.217.773.495 1.515.866 2.202a4.954 4.954 0 0 1-2.672-2.203m-.508-1.237a5.099 5.099 0 0 1-.16-1.237 5.1 5.1 0 0 1 .16-1.238h2.091c-.049.409-.086.817-.086 1.238s.037.829.086 1.237m2.698-6.168a8.425 8.425 0 0 1 1.181 2.456h-2.363a8.426 8.426 0 0 1 1.182-2.456m4.28 2.456h-1.824a9.682 9.682 0 0 0-.854-2.202 4.94 4.94 0 0 1 2.679 2.202m-4.281-3.712a6.193 6.193 0 0 0-6.187 6.187 6.186 6.186 0 0 0 6.187 6.186 6.186 6.186 0 0 0 6.186-6.186 6.186 6.186 0 0 0-6.186-6.187z" fill="#b3e5fc" stroke-width=".619"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-public-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#039be5"/><path d="M20.037 16.746c.05-.408.087-.817.087-1.237s-.037-.83-.087-1.238h2.091c.099.396.16.81.16 1.238a5.1 5.1 0 0 1-.16 1.237m-3.186 3.44c.371-.687.656-1.43.854-2.203h1.825a4.967 4.967 0 0 1-2.68 2.203m-.154-3.44h-2.895c-.062-.408-.099-.817-.099-1.237s.037-.835.099-1.238h2.895c.056.403.1.817.1 1.238s-.044.829-.1 1.237m-1.447 3.687a8.39 8.39 0 0 1-1.182-2.45h2.363a8.39 8.39 0 0 1-1.181 2.45m-2.475-7.399H13.06a4.902 4.902 0 0 1 2.672-2.202c-.371.686-.65 1.429-.866 2.202m-1.806 4.95h1.806c.217.773.495 1.515.866 2.202a4.954 4.954 0 0 1-2.672-2.203m-.508-1.237a5.099 5.099 0 0 1-.16-1.237 5.1 5.1 0 0 1 .16-1.238h2.091c-.05.409-.086.817-.086 1.238s.037.829.086 1.237m2.698-6.168a8.425 8.425 0 0 1 1.181 2.456h-2.363a8.426 8.426 0 0 1 1.182-2.456m4.28 2.456h-1.824a9.682 9.682 0 0 0-.854-2.202 4.941 4.941 0 0 1 2.679 2.202M17.34 9.322a6.193 6.193 0 0 0-6.187 6.187 6.186 6.186 0 0 0 6.187 6.186 6.186 6.186 0 0 0 6.186-6.186 6.186 6.186 0 0 0-6.186-6.187z" fill="#b3e5fc" stroke-width=".619"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-react-components" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#00bcd4" fill-rule="nonzero"/><path d="M16.473 13.927c.723 0 1.313.59 1.313 1.327 0 .703-.59 1.3-1.313 1.3a1.318 1.318 0 0 1-1.313-1.3c0-.737.59-1.327 1.313-1.327m-3.252 6.946c.443.267 1.412-.14 2.529-1.194a17.015 17.015 0 0 1-1.06-1.335 15.945 15.945 0 0 1-1.686-.252c-.358 1.502-.225 2.535.217 2.78m.499-4.03l-.204-.359a5.558 5.558 0 0 0-.203.604c.19.042.4.078.618.113l-.211-.359m4.593-.533l.569-1.054-.569-1.053c-.21-.372-.435-.702-.639-1.032-.38-.022-.78-.022-1.2-.022-.422 0-.823 0-1.202.022-.204.33-.428.66-.639 1.032l-.569 1.053.57 1.054c.21.372.434.702.638 1.032.38.021.78.021 1.201.021.421 0 .822 0 1.201-.02.204-.331.428-.661.639-1.033m-1.84-4.72c-.133.155-.274.316-.414.506h.828c-.14-.19-.28-.351-.414-.506m0 7.332c.133-.154.274-.316.414-.505h-.828c.14.19.28.35.414.505m3.245-9.284c-.436-.267-1.405.14-2.522 1.194.366.414.724.864 1.06 1.334.577.057 1.146.14 1.686.253.359-1.503.225-2.535-.224-2.78m-.492 4.03l.204.358c.077-.203.154-.407.203-.604-.19-.042-.4-.077-.618-.112l.211.358m1.018-4.95c1.033.589 1.145 2.141.71 3.953 1.784.527 3.069 1.398 3.069 2.584 0 1.187-1.285 2.058-3.07 2.585.436 1.812.324 3.364-.709 3.954-1.025.59-2.423-.085-3.77-1.37-1.35 1.285-2.747 1.96-3.78 1.37-1.025-.59-1.137-2.142-.702-3.954-1.783-.527-3.069-1.398-3.069-2.585s1.286-2.057 3.07-2.584c-.436-1.812-.324-3.364.702-3.954 1.032-.59 2.43.084 3.778 1.37 1.348-1.286 2.746-1.96 3.771-1.37m-.203 6.538c.239.527.45 1.054.625 1.588 1.475-.443 2.303-1.075 2.303-1.588 0-.512-.828-1.144-2.303-1.587a15.81 15.81 0 0 1-.625 1.587m-7.136 0a15.806 15.806 0 0 1-.625-1.587c-1.474.443-2.303 1.075-2.303 1.587 0 .513.829 1.145 2.303 1.588.176-.534.387-1.06.625-1.588m6.321 1.588l-.21.358c.217-.035.428-.07.617-.113-.049-.196-.126-.4-.203-.604l-.204.359m-2.03 2.837c1.117 1.053 2.086 1.46 2.522 1.194.45-.246.583-1.278.224-2.781-.54.112-1.11.196-1.685.253-.337.47-.695.92-1.06 1.334m-3.477-6.012l.21-.358c-.217.035-.428.07-.617.113.049.196.126.4.203.604l.204-.359m2.03-2.837c-1.117-1.053-2.086-1.46-2.529-1.194-.442.246-.576 1.278-.217 2.781.54-.112 1.11-.196 1.685-.253.337-.47.695-.92 1.06-1.334z" fill="#b2ebf2" stroke-width=".702"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-react-components-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#00bcd4"/><path d="M16.473 13.928c.723 0 1.313.59 1.313 1.327 0 .703-.59 1.3-1.313 1.3a1.318 1.318 0 0 1-1.313-1.3c0-.737.59-1.327 1.313-1.327m-3.252 6.946c.443.267 1.412-.14 2.529-1.194a16.997 16.997 0 0 1-1.06-1.335 15.945 15.945 0 0 1-1.686-.252c-.358 1.502-.225 2.535.217 2.78m.499-4.03l-.204-.359c-.077.204-.154.408-.203.604.19.042.4.078.618.113l-.211-.359m4.593-.533l.569-1.054-.57-1.053c-.21-.372-.434-.702-.638-1.032-.38-.022-.78-.022-1.201-.022-.421 0-.822 0-1.2.022-.205.33-.43.66-.64 1.032l-.569 1.053.569 1.054c.21.372.435.702.64 1.032.378.021.779.021 1.2.021.421 0 .822 0 1.2-.02.205-.33.43-.661.64-1.033m-1.84-4.72c-.133.155-.274.316-.414.506h.828c-.14-.19-.28-.351-.414-.506m0 7.332c.133-.154.274-.316.414-.505h-.828c.14.19.28.35.414.505m3.244-9.284c-.435-.267-1.404.14-2.52 1.194.364.414.723.864 1.06 1.334.575.057 1.144.14 1.685.253.358-1.503.225-2.535-.225-2.78m-.491 4.03l.203.358c.078-.203.155-.407.204-.604-.19-.042-.4-.077-.618-.112l.21.358m1.02-4.95c1.032.589 1.144 2.141.708 3.953 1.784.527 3.07 1.398 3.07 2.584 0 1.187-1.286 2.058-3.07 2.585.436 1.812.323 3.364-.709 3.954-1.025.59-2.423-.085-3.771-1.37-1.348 1.285-2.746 1.96-3.778 1.37-1.026-.59-1.138-2.142-.703-3.954-1.783-.527-3.069-1.398-3.069-2.585s1.286-2.057 3.07-2.584c-.436-1.812-.324-3.364.702-3.954 1.032-.59 2.43.084 3.778 1.37 1.348-1.286 2.746-1.96 3.771-1.37m-.204 6.538c.24.527.45 1.054.625 1.588 1.475-.443 2.304-1.075 2.304-1.588 0-.512-.829-1.144-2.304-1.587a15.81 15.81 0 0 1-.625 1.587m-7.135 0a15.808 15.808 0 0 1-.625-1.587c-1.475.443-2.303 1.075-2.303 1.587 0 .513.828 1.145 2.303 1.588.176-.534.386-1.06.625-1.588m6.32 1.588l-.21.358c.218-.035.428-.07.618-.113a5.56 5.56 0 0 0-.204-.604l-.203.359m-2.03 2.837c1.117 1.053 2.086 1.46 2.521 1.194.45-.246.583-1.278.225-2.781-.54.112-1.11.196-1.685.253-.338.47-.696.92-1.06 1.334m-3.477-6.012l.21-.358c-.217.035-.428.07-.617.112.049.197.126.4.203.604l.204-.358m2.03-2.837c-1.117-1.053-2.086-1.46-2.529-1.194-.442.246-.576 1.278-.217 2.781.54-.112 1.11-.196 1.685-.253.337-.47.695-.92 1.06-1.334z" fill="#b2ebf2" stroke-width=".702"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-actions" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ab47bc" fill-rule="nonzero"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#e1bee7" stroke="#e1bee7" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-actions-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ab47bc"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#e1bee7" stroke="#e1bee7" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-reducer" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ef5350" fill-rule="nonzero"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#ffcdd2" stroke="#ffcdd2" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-reducer-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ef5350"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#ffcdd2" stroke="#ffcdd2" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-store" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#8bc34a" fill-rule="nonzero"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#dcedc8" stroke="#dcedc8" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-store-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#8bc34a"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#dcedc8" stroke="#dcedc8" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-resource" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#fbc02d" fill-rule="nonzero"/><path d="M21.598 12.059h-6.085v-1.217h6.085m-2.434 6.085h-3.65V15.71h3.65m2.434-1.217h-6.085v-1.217h6.085m.608-4.26h-7.301a1.217 1.217 0 0 0-1.217 1.218v7.301a1.217 1.217 0 0 0 1.217 1.217h7.301a1.217 1.217 0 0 0 1.217-1.217v-7.301a1.217 1.217 0 0 0-1.217-1.217m-9.735 2.434h-1.217v8.518a1.217 1.217 0 0 0 1.217 1.217h8.519v-1.217H12.47z" fill="#fff9c4" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-resource-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#fbc02d"/><path d="M21.598 12.059h-6.085v-1.217h6.085m-2.434 6.085h-3.65V15.71h3.65m2.434-1.217h-6.085v-1.217h6.085m.608-4.26h-7.301a1.217 1.217 0 0 0-1.217 1.218v7.301a1.217 1.217 0 0 0 1.217 1.217h7.301a1.217 1.217 0 0 0 1.217-1.217v-7.301a1.217 1.217 0 0 0-1.217-1.217m-9.735 2.433h-1.217v8.519a1.217 1.217 0 0 0 1.217 1.217h8.519v-1.217H12.47z" fill="#fff9c4" stroke-width=".608"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" id="folder-sass" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#f8bbd0" fill-rule="nonzero"/><path d="M23.36 10.506c-.39-1.527-2.922-2.03-5.319-1.178-1.426.507-2.97 1.302-4.08 2.34-1.32 1.235-1.53 2.31-1.444 2.759.306 1.584 2.477 2.62 3.37 3.388v.005c-.264.13-2.19 1.104-2.64 2.1-.476 1.052.075 1.806.44 1.908 1.131.315 2.292-.251 2.916-1.182.602-.897.551-2.056.29-2.633.36-.095.781-.138 1.316-.076 1.508.177 1.804 1.118 1.748 1.513-.057.394-.373.61-.48.676-.105.065-.137.088-.129.137.013.07.062.068.152.053.125-.021.792-.321.821-1.048.037-.924-.849-1.958-2.416-1.93-.646.01-1.052.072-1.345.181-.022-.024-.044-.05-.067-.073-.969-1.034-2.76-1.765-2.684-3.156.027-.505.203-1.835 3.442-3.45 2.653-1.322 4.777-.957 5.145-.151.524 1.152-1.136 3.293-3.891 3.601-1.05.118-1.603-.289-1.74-.44-.145-.16-.166-.167-.22-.137-.088.049-.033.19 0 .274.082.214.42.594.995.782.506.166 1.739.258 3.23-.319 1.669-.646 2.972-2.443 2.59-3.944zm-7.103 7.783a2.2 2.2 0 0 1-.065 1.413 2.405 2.405 0 0 1-.453.704c-.5.546-1.198.752-1.497.579-.323-.188-.161-.956.418-1.568.623-.66 1.52-1.083 1.52-1.083l-.002-.002.079-.043z" fill="#ec407a" fill-rule="nonzero" stroke="#ec407a" stroke-width=".5199012000000001"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" id="folder-sass-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#f8bbd0" fill-rule="nonzero"/><path d="M23.36 10.506c-.39-1.527-2.922-2.03-5.319-1.178-1.426.507-2.97 1.302-4.08 2.34-1.32 1.235-1.53 2.31-1.444 2.759.306 1.584 2.477 2.62 3.37 3.388v.005c-.264.13-2.19 1.104-2.64 2.1-.476 1.052.075 1.806.44 1.908 1.131.315 2.292-.251 2.916-1.182.602-.897.551-2.056.29-2.633.36-.095.781-.138 1.316-.076 1.508.177 1.804 1.118 1.748 1.513-.057.394-.373.61-.48.676-.105.065-.137.088-.129.137.013.07.062.068.152.053.125-.021.792-.321.821-1.048.037-.924-.849-1.958-2.416-1.93-.646.01-1.052.072-1.345.181-.022-.024-.044-.05-.067-.073-.969-1.034-2.76-1.765-2.684-3.156.027-.505.203-1.835 3.442-3.45 2.653-1.322 4.777-.957 5.145-.151.524 1.152-1.136 3.293-3.891 3.601-1.05.118-1.603-.289-1.74-.44-.145-.16-.166-.167-.22-.137-.088.049-.033.19 0 .274.082.214.42.594.995.782.506.166 1.739.258 3.23-.319 1.669-.646 2.972-2.443 2.59-3.944zm-7.103 7.783a2.2 2.2 0 0 1-.065 1.413 2.405 2.405 0 0 1-.453.704c-.5.546-1.198.752-1.497.579-.323-.188-.161-.956.418-1.568.623-.66 1.52-1.083 1.52-1.083l-.002-.002.079-.043z" fill="#ec407a" fill-rule="nonzero" stroke="#ec407a" stroke-width=".5199012000000001"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-scripts" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#546e7a" fill-rule="nonzero"/><path d="M18.466 20.241c.69 0 1.259-.568 1.259-1.258v-8.18H15.32a.632.632 0 0 0-.63.63v6.292h-1.887v-6.922c0-1.036.852-1.888 1.888-1.888h6.921c1.036 0 1.888.852 1.888 1.888v.63h-2.517v8.18a1.896 1.896 0 0 1-1.888 1.887h-6.292a1.896 1.896 0 0 1-1.888-1.888v-.629h6.293c0 .69.568 1.258 1.258 1.258z" fill-rule="nonzero" fill="#cfd8dc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-scripts-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#546e7a" fill-rule="nonzero"/><path d="M18.466 20.241c.69 0 1.259-.568 1.259-1.258v-8.18H15.32a.632.632 0 0 0-.63.63v6.292h-1.887v-6.922c0-1.036.852-1.888 1.888-1.888h6.921c1.036 0 1.888.852 1.888 1.888v.63h-2.517v8.18a1.896 1.896 0 0 1-1.888 1.887h-6.292a1.896 1.896 0 0 1-1.888-1.888v-.629h6.293c0 .69.568 1.258 1.258 1.258z" fill-rule="nonzero" fill="#cfd8dc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-src" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#4caf50" fill-rule="nonzero"/><g fill="#c8e6c9" transform="translate(2.065 -.225) scale(.70678)"><path d="M19.146 30.989a.902.902 0 0 1-.207-.025 1.045 1.045 0 0 1-.726-1.213l2.709-14.431c.049-.279.209-.525.444-.683a.891.891 0 0 1 .7-.122c.519.152.837.684.727 1.213L20.077 30.16a1.032 1.032 0 0 1-.442.681.895.895 0 0 1-.489.148zM24.578 28.944h-.068a.932.932 0 0 1-.668-.377 1.104 1.104 0 0 1 .1-1.419l4.658-4.553-4.638-4.239a1.105 1.105 0 0 1-.141-1.416.938.938 0 0 1 .661-.4.9.9 0 0 1 .709.237l5.47 5c.386.372.448.974.144 1.416a1.05 1.05 0 0 1-.142.163l-5.447 5.324a.913.913 0 0 1-.638.264zM16.423 28.947a.917.917 0 0 1-.639-.267l-5.452-5.327a.874.874 0 0 1-.132-.153 1.097 1.097 0 0 1 .141-1.414l5.471-5a.882.882 0 0 1 .7-.238.939.939 0 0 1 .665.4 1.104 1.104 0 0 1-.14 1.417L12.4 22.6l4.659 4.551c.377.382.42.988.1 1.419a.928.928 0 0 1-.669.377z" fill-rule="nonzero"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-src-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#4caf50"/><g fill="#c8e6c9" fill-rule="evenodd" transform="translate(2.064 -.224) scale(.70678)"><path d="M19.146 30.989a.902.902 0 0 1-.207-.025 1.045 1.045 0 0 1-.726-1.213l2.709-14.431c.049-.279.209-.525.444-.683a.891.891 0 0 1 .7-.122c.519.152.837.684.727 1.213L20.077 30.16a1.032 1.032 0 0 1-.442.681.895.895 0 0 1-.489.148zM24.578 28.944h-.068a.932.932 0 0 1-.668-.377 1.104 1.104 0 0 1 .1-1.419l4.658-4.553-4.638-4.239a1.105 1.105 0 0 1-.141-1.416.938.938 0 0 1 .661-.4.9.9 0 0 1 .709.237l5.47 5c.386.372.448.974.144 1.416a1.05 1.05 0 0 1-.142.163l-5.447 5.324a.913.913 0 0 1-.638.264zM16.423 28.947a.917.917 0 0 1-.639-.267l-5.452-5.327a.874.874 0 0 1-.132-.153 1.097 1.097 0 0 1 .141-1.414l5.471-5a.882.882 0 0 1 .7-.238.939.939 0 0 1 .665.4 1.104 1.104 0 0 1-.14 1.417L12.4 22.6l4.659 4.551c.377.382.42.988.1 1.419a.928.928 0 0 1-.669.377z" fill-rule="nonzero"/></g></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-test" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#1de9b6" fill-rule="nonzero"/><path d="M14 8.097v1.39h.695v9.732A2.794 2.794 0 0 0 17.475 22a2.794 2.794 0 0 0 2.781-2.78V9.486h.695v-1.39H14m2.78 9.732c-.417 0-.695-.278-.695-.695 0-.417.278-.695.696-.695.417 0 .695.278.695.695 0 .417-.278.695-.695.695m1.39-2.78c-.417 0-.695-.278-.695-.696 0-.417.278-.695.695-.695.417 0 .695.278.695.695 0 .418-.278.696-.695.696m.695-3.476h-2.78V9.487h2.78v2.086z" fill="#00897b" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-test-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#1de9b6" fill-rule="nonzero"/><path d="M14 8.097v1.39h.695v9.732A2.794 2.794 0 0 0 17.475 22a2.794 2.794 0 0 0 2.781-2.78V9.486h.695v-1.39H14m2.78 9.732c-.417 0-.695-.278-.695-.695 0-.417.278-.695.696-.695.417 0 .695.278.695.695 0 .417-.278.695-.695.695m1.39-2.78c-.417 0-.695-.278-.695-.696 0-.417.278-.695.695-.695.417 0 .695.278.695.695 0 .418-.278.696-.695.696m.695-3.476h-2.78V9.487h2.78v2.086z" fill="#00897b" fill-rule="nonzero"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-tools" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#1e88e5" fill-rule="nonzero"/><path d="M21.043 15.266a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141-2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-3.569 0a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141 2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m3.925-6.424a6.424 6.424 0 0 0-6.423 6.424 6.424 6.424 0 0 0 6.423 6.424 1.07 1.07 0 0 0 1.071-1.07c0-.28-.107-.53-.278-.715a1.105 1.105 0 0 1-.271-.713 1.07 1.07 0 0 1 1.07-1.071h1.263a3.569 3.569 0 0 0 3.57-3.569c0-3.154-2.877-5.71-6.425-5.71z" fill="#bbdefb" stroke-width=".714"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-tools-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#1e88e5"/><path d="M21.043 15.266a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141-2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-3.569 0a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141 2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m3.925-6.424a6.424 6.424 0 0 0-6.423 6.424 6.424 6.424 0 0 0 6.423 6.424 1.07 1.07 0 0 0 1.071-1.07c0-.28-.107-.53-.278-.715a1.105 1.105 0 0 1-.271-.713 1.07 1.07 0 0 1 1.07-1.071h1.263a3.569 3.569 0 0 0 3.57-3.569c0-3.154-2.877-5.71-6.425-5.71z" fill="#bbdefb" stroke-width=".714"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-views" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ff8a65" fill-rule="nonzero"/><path d="M12.487 21.868L11.384 9.5H23.5l-1.104 12.366-4.961 1.375-4.948-1.373zm4.464-3.2l-3.926-2.36v-.855l3.926-2.361v1.323l-2.504 1.465 2.504 1.465v1.323zm.982-.001v-1.323l2.522-1.464-2.522-1.464v-1.323l3.926 2.35v.874l-3.926 2.35z" fill="#e44d26"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-views-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ff8a65" fill-rule="nonzero"/><path d="M12.487 21.868L11.384 9.5H23.5l-1.104 12.366-4.961 1.375-4.948-1.373zm4.464-3.2l-3.926-2.36v-.855l3.926-2.361v1.323l-2.504 1.465 2.504 1.465v1.323zm.982-.001v-1.323l2.522-1.464-2.522-1.464v-1.323l3.926 2.35v.874l-3.926 2.35z" fill="#e44d26"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vscode" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#42a5f5" fill-rule="nonzero"/><path d="M20.811 8.52l-5.988 5.506-3.346-2.522-1.383.805 3.298 3.03-3.298 3.032 1.383.807 3.346-2.522 5.988 5.503 2.921-1.419V9.94zm0 3.622v6.396l-4.245-3.198z" fill="#bbdefb" stroke-width=".974"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vscode-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#42a5f5" fill-rule="nonzero"/><path d="M20.81 8.52l-5.988 5.506-3.346-2.522-1.384.805 3.3 3.03-3.3 3.032 1.384.807 3.346-2.522 5.988 5.503 2.921-1.419V9.94zm0 3.621v6.397l-4.245-3.198z" fill="#bbdefb" stroke-width=".974"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vue" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#009688" fill-rule="nonzero"/><g transform="translate(8.459 6.362) scale(.69572)"><path d="M1.821 4.15l10.21 17.618L22.239 4.235v-.084h-7.692l-2.434 4.178-2.422-4.178z" fill="#41b883"/><path d="M5.937 4.15l6.152 10.616 6.18-10.617h-3.722l-2.434 4.179-2.422-4.179z" fill="#35495e"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vue-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#009688"/><g transform="translate(8.458 6.362) scale(.69572)"><path d="M1.821 4.15l10.21 17.618L22.239 4.235v-.084h-7.692l-2.434 4.178-2.422-4.178z" fill="#41b883"/><path d="M5.937 4.15l6.152 10.616 6.18-10.617h-3.722l-2.434 4.179-2.422-4.179z" fill="#35495e"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-webpack" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#03a9f4" fill-rule="nonzero"/><g transform="translate(9.192 7.48) scale(.66328)"><path d="M19.376 15.988l-7.708 4.45-7.709-4.45v-8.9l7.709-4.451 7.708 4.45z" fill="#fff" fill-opacity=".785"/><path d="M12.286 1.98c-.21 0-.41.059-.57.179l-7.9 4.44c-.32.17-.53.5-.53.88v9c0 .38.21.711.53.881l7.9 4.44c.16.12.36.18.57.18s.41-.06.57-.18l7.9-4.44c.32-.17.53-.5.53-.88v-9c0-.38-.21-.712-.53-.882l-7.9-4.44a.945.945 0 0 0-.57-.179zm0 2.15l7 3.94v2.103h-.016v5.177h.016v.54l-7 3.939-7-3.94V8.07zm0 2.08l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#8ed6fb"/><path d="M12.286 6.21l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#1c78c0"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-webpack-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#03a9f4"/><g transform="translate(9.193 7.48) scale(.66328)"><path d="M19.376 15.988l-7.708 4.45-7.709-4.45v-8.9l7.709-4.451 7.708 4.45z" fill="#fff" fill-opacity=".785"/><path d="M12.286 1.98c-.21 0-.41.059-.57.179l-7.9 4.44c-.32.17-.53.5-.53.88v9c0 .38.21.711.53.881l7.9 4.44c.16.12.36.18.57.18s.41-.06.57-.18l7.9-4.44c.32-.17.53-.5.53-.88v-9c0-.38-.21-.712-.53-.882l-7.9-4.44a.945.945 0 0 0-.57-.179zm0 2.15l7 3.94v2.103h-.016v5.177h.016v.54l-7 3.939-7-3.94V8.07zm0 2.08l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#8ed6fb"/><path d="M12.286 6.21l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#1c78c0"/></g></symbol><symbol viewBox="0 0 24 24" id="font" xmlns="http://www.w3.org/2000/svg"><path d="M9.62 12L12 5.67 14.37 12M11 3L5.5 17h2.25l1.12-3h6.25l1.13 3h2.25L13 3h-2z" fill="#f44336"/></symbol><symbol viewBox="0 0 500 500" id="fsharp" xmlns="http://www.w3.org/2000/svg"><path d="M235.906 36.66L21.963 250.601l213.943 213.943v-84.36L106.209 250.487l129.697-129.696z" fill="#378bba" stroke-width="14.706"/><path d="M235.906 156.614l-93.622 93.62 93.622 93.622z" fill="#378bba" stroke-width="15.006"/><path d="M263.417 36.64L477.36 250.583 263.417 464.526v-84.36l129.696-129.697-129.696-129.696z" fill="#30b9db" stroke-width="14.706"/></symbol><symbol viewBox="0 0 152.99 160.01" id="fusebox" xmlns="http://www.w3.org/2000/svg"><defs id="fkdefs4"><style id="fkstyle2">.fkcls-1{fill:#fff}.fkcls-2{fill:#515151}.fkcls-3{fill:#1d79bf}.fkcls-4{fill:#383838}</style></defs><title id="fktitle6">Asset 3</title><g id="fkLayer_2" data-name="Layer 2" transform="matrix(.87285 0 0 .87285 10.17 10.175)"><g id="fkFuse_Box" data-name="Fuse Box"><g id="fkLOGO"><path class="fkcls-1" id="fkpolygon8" fill="#fff" d="M76.56 2.19l74.22 24.93-7.7 87.77-65.41 42.66-69.79-43.93-5.7-86.13z"/><path class="fkcls-2" d="M77.69 160L5.87 114.81 0 26 76.55 0 153 25.67l-7.94 90.4zM9.88 112.43l67.77 42.66 63.45-41.39 7.47-85.13-72-24.18L4.36 28.95z" id="fkpath10" fill="#515151"/><path class="fkcls-3" id="fkpolygon12" fill="#1d79bf" d="M76.4 148.8V61.68l66.93-29.82-5.99 78.77z"/><path id="fkF" class="fkcls-4" fill="#383838" d="M76.4 148.8l-60.35-37.39L9.63 31.8 76.4 61.68z"/><path class="fkcls-1" d="M25.58 52.73l.54 15.93 37.35 18.18.12 14.69-37-18.21 1.64 37.1-14.56-9-5.05-80.55 67.79 30.82v15.46z" id="fkpath15" fill="#fff"/><path class="fkcls-1" d="M135.91 90.77c-.08 13.12-6.33 26.59-16.77 33.12l-42.8 27.93V61.71l42.27-18.84c5.16-2.41 9.51-1.43 12.4 3.11 1.9 3 2.89 7.23 2.86 12.21A35.69 35.69 0 0 1 129.34 76c4.29 2 6.66 6.55 6.57 14.77zM123 63.76c0-4.64-2-6.93-4.92-5.45l-29 14.48L89 90l29.44-15.59c2.5-1.32 4.56-5.91 4.56-10.65zM125.15 96c0-5.71-2.42-8.24-6.55-5.93L89 106.64v19.58l29.34-17.46c4.43-2.64 6.79-7.27 6.81-12.76z" id="fkpath17" fill="#fff"/><path id="fkTOP" class="fkcls-4" fill="#383838" d="M76.4 8.82L9.71 31.77l109.77 2.38-84.02 9.21L76.4 61.68l20.76-9.25-27.73-1.37 49.78-8.46 24.12-10.74z"/></g></g></g></symbol><symbol viewBox="0 0 24 24" id="git" xmlns="http://www.w3.org/2000/svg"><path d="M2.6 10.59L8.38 4.8l1.69 1.7c-.24.85.15 1.78.93 2.23v5.54c-.6.34-1 .99-1 1.73a2 2 0 0 0 2 2 2 2 0 0 0 2-2c0-.74-.4-1.39-1-1.73V9.41l2.07 2.09c-.07.15-.07.32-.07.5a2 2 0 0 0 2 2 2 2 0 0 0 2-2 2 2 0 0 0-2-2c-.18 0-.35 0-.5.07L13.93 7.5a1.98 1.98 0 0 0-1.15-2.34c-.43-.16-.88-.2-1.28-.09L9.8 3.38l.79-.78c.78-.79 2.04-.79 2.82 0l7.99 7.99c.79.78.79 2.04 0 2.82l-7.99 7.99c-.78.79-2.04.79-2.82 0L2.6 13.41c-.79-.78-.79-2.04 0-2.82z" fill="#e64a19"/></symbol><symbol viewBox="0 0 494 455" id="gitlab" xmlns="http://www.w3.org/2000/svg"><title>logo</title><defs><path id="fma" d="M0 1173.3h2000V0H0v1173.3z"/></defs><g transform="matrix(.88256 0 0 -.88256 -286.767 742.766)" fill="none" fill-rule="evenodd"><mask id="fmb" fill="#fff"><use width="100%" height="100%" xlink:href="#fma"/></mask><g mask="url(#fmb)"><g transform="translate(358.67 358.67)"><path d="M492.532 195.445l-27.559 84.815-54.617 168.1c-2.81 8.648-15.045 8.648-17.856 0l-54.619-168.1h-181.37l-54.62 168.1c-2.81 8.648-15.045 8.648-17.856 0l-54.617-168.1-27.557-84.815a18.775 18.775 0 0 1 6.82-20.992l238.51-173.29 238.51 173.29a18.777 18.777 0 0 1 6.82 20.992" fill="#fc6d26"/><path d="M247.2 1.16l90.684 279.1h-181.37z" fill="#e24329"/><path d="M247.201 1.16l-90.684 279.09H29.427z" fill="#fc6d26"/><path d="M29.422 280.256L1.862 195.44a18.774 18.774 0 0 1 6.822-20.991L247.194 1.16z" fill="#fca326"/><path d="M29.422 280.26h127.09l-54.619 168.1c-2.81 8.65-15.047 8.65-17.856 0z" fill="#e24329"/><path d="M247.2 1.16l90.684 279.09h127.09z" fill="#fc6d26"/><path d="M464.98 280.256l27.559-84.815a18.774 18.774 0 0 0-6.821-20.991L247.208 1.16z" fill="#fca326"/><path d="M464.97 280.26H337.88l54.619 168.1c2.81 8.65 15.047 8.65 17.856 0z" fill="#e24329"/></g></g></g></symbol><symbol viewBox="0 0 24 24" id="go" xmlns="http://www.w3.org/2000/svg"><path d="M10.575 1.695c-2.634 0-4.756 2.453-4.756 5.502v4.6l-.027-.003v4.71c0 3.05 2.123 5.502 4.757 5.502h2.286c2.634 0 4.757-2.453 4.757-5.502v-4.6a5.1 5.1 0 0 0 .026.003v-4.71c0-3.049-2.122-5.502-4.756-5.502h-2.287z" fill="#73cddc"/><rect width="2.289" height="3.335" x="-1.178" y="6.092" ry="1.125" transform="matrix(.4849 -.87457 .85979 .51065 0 0)" fill="#73cddc"/><rect width="2.297" height="3.39" x="10.261" y="-15.076" ry="1.143" transform="matrix(.44646 .8948 -.89204 .45195 0 0)" fill="#73cddc"/><circle cx="9.267" cy="5.13" r="2.054" fill="#fff" stroke="#5e5d5b" stroke-width=".1"/><circle cx="14.214" cy="5.116" r="2.054" fill="#fff" stroke="#5e5d5b" stroke-width=".1"/><ellipse cx="8.039" cy="5.051" rx=".792" ry=".901" fill="#030d18"/><path d="M11.792 9.556l.763.138a.403.689 0 0 1 .008.138.403.689 0 0 1-.402.69.403.689 0 0 1-.404-.69.403.689 0 0 1 .035-.276z" fill="#fff" stroke="#fff" stroke-width=".155"/><ellipse cx="8.51" cy="5.365" rx=".138" ry=".166" fill="#fff"/><ellipse cx="12.945" cy="5.189" rx=".792" ry=".901" fill="#030d18"/><ellipse cx="13.414" cy="5.446" rx=".138" ry=".166" fill="#fff"/><ellipse cx="-12.982" cy="-3.409" rx=".708" ry="1.026" transform="rotate(-129.403)" fill="#f6d2a1" stroke-width=".4"/><path d="M11.772 9.553l-.757.135a.4.672 0 0 0-.008.135.4.672 0 0 0 .4.672.4.672 0 0 0 .4-.672.4.672 0 0 0-.035-.27z" fill="#fff" stroke="#fff" stroke-width=".153"/><ellipse cx="1.841" cy="-21.563" rx=".707" ry="1.026" transform="scale(1 -1) rotate(50.597)" fill="#f6d2a1" stroke-width=".4"/><ellipse cx="-17.281" cy="-21.784" rx=".864" ry="1.27" transform="matrix(.3054 -.95222 -.97065 -.2405 0 0)" fill="#f6d2a1" stroke-width=".4"/><ellipse cx="22.885" cy="2.587" rx=".864" ry="1.27" transform="matrix(.22652 .974 .95652 -.29167 0 0)" fill="#f6d2a1" stroke-width=".4"/><path d="M10.708 8.392a.594.594 0 0 0-.594.597v.115c0 .331.264.598.594.598h.386a.973.772 0 0 1 .697-.235.973.772 0 0 1 .698.235h.334c.33 0 .594-.267.594-.598V8.99a.595.595 0 0 0-.594-.597h-2.115z" fill="#f6d2a1" stroke="#657075" stroke-width=".1"/><ellipse cx="11.734" cy="8.203" rx="1.208" ry=".68" fill="#030d18" stroke="#fff" stroke-width=".162"/></symbol><symbol viewBox="0 0 24 24" id="gradle" xmlns="http://www.w3.org/2000/svg"><path d="M21.718 5.503c-.731-1.315-2.04-1.708-2.963-1.727-1.133-.023-2.065.605-1.888 1.017.037.088.25.55.38.741.19.275.527.064.646 0 .353-.187.73-.248 1.16-.198.409.048.954.3 1.319 1.001.859 1.652-1.794 5.05-5.114 2.697-3.32-2.353-6.548-1.574-8.01-1.1-1.462.475-2.135.952-1.556 2.055.785 1.498.524 1.038 1.285 2.28 1.21 1.97 3.856-.908 3.856-.908-1.972 2.906-3.662 2.204-4.31 1.188a15.864 15.864 0 0 1-1.038-1.97c-4.993 1.76-3.642 9.534-3.642 9.534h2.48c.632-2.862 2.892-2.757 3.28 0h1.892c1.673-5.59 5.914 0 5.914 0h2.466c-.69-3.812 1.388-5.01 2.697-7.246 1.31-2.235 2.551-4.969 1.146-7.364zm-6.362 7.362c-1.304-.426-.837-1.723-.837-1.723s1.139.368 2.68.87c-.09.403-.856 1.175-1.843.853z" fill="#0097a7" stroke-width=".47"/></symbol><symbol preserveAspectRatio="xMidYMid" viewBox="0 0 300 300" id="graphcool" xmlns="http://www.w3.org/2000/svg"><path d="M246.886 107.727c-12.237-6.892-27.616 2.1-30.081 3.646l-52.834 29.965c-7.8-6.196-18.914-5.933-26.412.625-7.499 6.558-9.24 17.537-4.14 26.094 5.102 8.556 15.588 12.246 24.923 8.768 9.335-3.478 14.852-13.129 13.111-22.937l52.688-29.9.321-.196c3.464-2.188 11.5-5.462 15.256-3.34 2.706 1.524 4.252 6.629 4.376 14.148h-.066v66.092a17.313 17.313 0 0 1-8.635 14.95l-75.739 43.755a17.312 17.312 0 0 1-17.261 0l-75.74-43.756a17.312 17.312 0 0 1-8.634-14.95V113.22c.01-6.165 3.3-11.86 8.634-14.95l68.549-39.562c6.522 7.482 17.451 9.25 26 4.206s12.283-15.468 8.886-24.794c-3.397-9.327-12.962-14.904-22.751-13.27-9.79 1.636-17.022 10.02-17.204 19.944L59.397 85.632a31.932 31.932 0 0 0-15.978 27.588v87.454a31.933 31.933 0 0 0 15.927 27.602l75.74 43.755a31.934 31.934 0 0 0 31.846 0l75.74-43.755a31.933 31.933 0 0 0 15.927-27.58V137.12h.05c.373-14.913-3.616-24.794-11.762-29.389z" fill="#27ae60" stroke="#27ae60" stroke-width="7.883622079999999"/></symbol><symbol viewBox="0 0 400 400" id="graphql" xmlns="http://www.w3.org/2000/svg"><path d="M67.008 293.022l-13.143-7.588L200.282 31.839l13.143 7.588z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M50.855 265.174H343.69v15.177H50.855z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M203.122 358.269L56.649 273.7l7.589-13.143 146.472 84.568zm127.24-220.407L183.889 53.293l7.589-13.143 146.472 84.568z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M64.278 137.803l-7.588-13.142 146.472-84.568 7.588 13.143z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M327.661 293.025L181.244 39.43l13.143-7.589 146.417 253.596zM62.466 114.597h15.176v169.136H62.466zm254.528 0h15.176v169.136h-15.176z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M200.538 351.845l-6.628-11.481L321.3 266.812l6.629 11.48z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M352.284 288.67c-8.777 15.268-28.342 20.48-43.61 11.703-15.268-8.777-20.48-28.342-11.703-43.61 8.777-15.268 28.342-20.48 43.61-11.703 15.36 8.869 20.57 28.342 11.703 43.61M97.574 141.567c-8.778 15.268-28.343 20.48-43.61 11.703-15.269-8.777-20.48-28.342-11.703-43.61 8.777-15.268 28.342-20.48 43.61-11.703 15.268 8.869 20.479 28.342 11.702 43.61M42.353 288.67c-8.777-15.268-3.566-34.741 11.702-43.61 15.268-8.776 34.741-3.565 43.61 11.703 8.776 15.268 3.565 34.741-11.703 43.61-15.36 8.776-34.833 3.565-43.61-11.703m254.71-147.103c-8.776-15.268-3.565-34.741 11.703-43.61 15.268-8.776 34.742-3.565 43.61 11.703 8.777 15.268 3.566 34.741-11.702 43.61-15.268 8.776-34.833 3.565-43.61-11.703m-99.745 236.608c-17.645 0-31.907-14.262-31.907-31.907s14.262-31.907 31.907-31.907 31.907 14.262 31.907 31.907c0 17.554-14.262 31.907-31.907 31.907m0-294.206c-17.645 0-31.907-14.262-31.907-31.907s14.262-31.907 31.907-31.907 31.907 14.262 31.907 31.907-14.262 31.907-31.907 31.907" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/></symbol><symbol viewBox="0 0 24 24" id="groovy" xmlns="http://www.w3.org/2000/svg"><path d="M12 1.982a10.119 10.119 0 0 0-10.12 10.12A10.119 10.119 0 0 0 12 22.22 10.119 10.119 0 0 0 22.12 12.1 10.119 10.119 0 0 0 12 1.983zm1.254 2.422c.91 0 1.647.261 2.213.78.571.518.857 1.188.857 2.013 0 .889-.319 1.673-.959 2.35-.64.677-1.376 1.015-2.207 1.015-.486 0-.89-.119-1.213-.357-.317-.238-.476-.532-.476-.88 0-.212.06-.4.181-.563.127-.164.274-.246.438-.246.159 0 .238.092.238.277 0 .164.06.29.182.38.121.09.261.136.42.136.423 0 .828-.29 1.215-.866.391-.582.587-1.202.587-1.863 0-.465-.151-.844-.453-1.135-.301-.296-.69-.445-1.166-.445-.714 0-1.406.318-2.078.953-.666.635-1.211 1.47-1.635 2.506-.417 1.031-.627 2.014-.627 2.945 0 .857.185 1.54.555 2.047.37.503.863.754 1.477.754 1.037 0 2.027-.734 2.974-2.2l1.493-.212c.185-.026.277.018.277.135 0 .053-.072.28-.215.681-.143.402-.337 1.074-.586 2.016.82-.476 1.455-1.003 1.904-1.58v.914c-.36.418-1.046.888-2.062 1.412-.212 1.407-.682 2.493-1.406 3.26-.725.772-1.54 1.16-2.444 1.16-.433 0-.775-.102-1.023-.303-.243-.2-.365-.477-.365-.832 0-.984.955-1.94 2.865-2.865.2-.714.395-1.356.586-1.928-.333.482-.817.907-1.451 1.278-.635.37-1.225.554-1.77.554-.889 0-1.628-.383-2.22-1.15-.588-.772-.881-1.748-.881-2.928 0-1.243.333-2.42 1-3.531a7.747 7.747 0 0 1 2.625-2.674c1.084-.672 2.134-1.008 3.15-1.008zM12.03 16.592c-1.375.687-2.062 1.365-2.062 2.031 0 .354.169.533.508.533.666 0 1.184-.856 1.554-2.564z" fill="#26c6da"/></symbol><symbol viewBox="0 0 24 24" id="gulp" xmlns="http://www.w3.org/2000/svg"><path d="M8.37 15.94a596.238 596.238 0 0 1-.482-4.982c.002-.042-.225-.077-.505-.077h-.508V8.95h3.966V5.198l1.871-1.124c1.14-.685 1.978-1.125 2.144-1.125.4 0 .866.506.866.939 0 .19-.057.422-.127.517-.07.095-.722.53-1.45.966l-1.321.792-.029 1.393-.028 1.393h3.972v1.932h-.98l-.495 4.983-.495 4.983H8.854l-.485-4.906z" fill="#e53935"/></symbol><symbol viewBox="0 0 24 24" id="h" xmlns="http://www.w3.org/2000/svg"><path d="M16.745 19.818h-3.007v-5.882q0-2.381-1.736-2.381-.869 0-1.438.663-.56.662-.56 1.718v5.882H6.988V4.533h3.016v6.508h.037q1.186-1.802 3.193-1.802 3.511 0 3.511 4.239z" stroke-width=".478" fill="#0277bd"/></symbol><symbol viewBox="0 0 253.6 253.6" id="hack" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-29.243 -29.515) scale(1.2301)"><path fill="#607d8b" d="M69.496 159.551v52.576l51.77-52.576zM123.507 41.523l-54.01 52.755v55.084l54.01-54.009z"/><path fill="#eceff1" d="M130.023 95.663v51.501l52.128-51.5z"/><path fill="#607d8b" d="M185.465 101.867l-55.442 55.174v55.083l55.442-55.262z"/><path fill="#ffa000" d="M73.068 154.283l50.427.09v-50.248z"/></g></symbol><symbol viewBox="0 0 300 300.00001" id="haml" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 165.6)"><path d="M78.42-132.307c-12.047-.302-26.924 5.998-26.924 5.998l49.195 99.791L74.605 85.005c23.81 20.134 50.07 10.504 50.07 10.504L136.76 9.212c1.526 1.446 3.146 2.77 4.777 3.995 5.244 3.714 10.925 6.553 16.606 8.738 5.68 2.185 11.583 3.933 17.482 5.244 3.933.874 7.645 1.53 11.578 1.967-1.748 3.933-2.84 8.083-2.621 12.672 0 .437.22.873.656 1.092h.217c4.152 2.185 8.521 3.934 13.328 5.027 4.589.874 9.615 1.312 14.422.656 5.026-.655 10.051-2.623 13.984-5.9 3.933-3.278 6.774-7.648 8.522-12.237l.219-.218v-.217l.656-5.899v-.22c2.185-1.311 4.37-2.621 6.555-4.37 2.622-2.184 5.025-4.589 6.773-7.648 1.748-3.059 2.84-6.774 2.621-10.488-.218-3.496-1.53-6.99-3.06-10.049-1.53-3.059-3.495-5.901-5.68-8.523-4.37-5.026-9.614-9.176-15.295-12.454-5.462-3.496-11.581-6.338-17.7-8.304l-2.404-.656-1.962-.655c-1.311-.437-2.406-1.092-3.498-1.53-2.185-1.31-3.717-2.622-4.809-4.37-2.185-3.278-2.403-8.301-1.31-13.545.218-1.311.656-2.623 1.093-3.934a96.064 96.064 0 0 0 1.31-4.152c.314-1.412.51-2.829.598-4.402l29.203-25.553c-2.275-8.404-27.488-17.158-27.488-17.158l-74.931 63.726-43.243-81.584c-1.553-.35-3.218-.527-4.94-.57zm107.682 73.14c-.449 2.336-.647 4.795-.647 7.258.219 3.715 1.311 7.87 3.715 11.366 2.403 3.496 5.68 6.117 8.957 7.646a29.663 29.663 0 0 0 5.027 1.967l2.623.654 2.184.438c5.68 1.53 11.142 3.714 16.168 6.554 5.025 2.84 9.833 6.337 13.766 10.27s6.992 8.959 7.43 13.984c.218 3.496-.22 6.118-1.313 8.303-1.093 2.404-2.84 4.588-4.807 6.555-.874.874-1.966 1.747-2.84 2.402a27.11 27.11 0 0 0-.654-5.898c-.219-1.093-.438-1.966-.875-3.059-.437-.874-.872-1.966-1.965-2.621-.218 0-.44-.001-.44.217-1.31 3.277-3.494 6.12-5.898 8.086-2.403 1.966-5.462 2.84-8.521 3.058-3.06.219-6.338-.436-9.616-1.31-3.277-.874-6.552-1.968-9.83-3.06l-.439-.22c-.656-.218-1.526.002-1.963.44-1.748 2.185-3.06 4.149-4.59 6.334a58.435 58.435 0 0 0-2.84 5.027c-3.933-1.53-7.649-2.841-11.582-4.37-5.462-2.186-10.925-4.37-15.95-6.991-5.245-2.404-10.268-5.246-14.638-8.524-3.15-2.363-6.062-4.845-8.185-7.681l2.404-17.172z" fill="#f4511e" stroke-width="0" stroke-linejoin="round"/></g></symbol><symbol viewBox="0 0 24 24" id="handlebars" xmlns="http://www.w3.org/2000/svg"><path d="M8.55 10.32c-2.753 0-4.202 3.48-5.793 3.48-.98 0-1.126-.677-1.126-.915 0-.332.236-.706.564-.706.59 0 .414.77.414.77s.798-.555.272-1.298c-.42-.595-1.31-.623-1.92-.17-.617.458-1.057 1.146-.853 2.287.1.551.468 1.35 1.233 1.805.764.455 1.925.566 2.335.566 2.194 0 4.342-1.633 6.639-2.322a5.513 5.513 0 0 1 1.497-.222 6.19 6.19 0 0 1 1.92.226c2.296.689 4.444 2.323 6.638 2.323.41 0 1.57-.11 2.335-.566.765-.455 1.132-1.256 1.231-1.807.204-1.14-.235-1.829-.853-2.287-.61-.453-1.497-.423-1.918.172-.526.743.27 1.297.27 1.297s-.176-.77.414-.77c.329 0 .565.373.565.705 0 .238-.147.914-1.126.914-1.592 0-3.04-3.478-5.794-3.478-2.565 0-3.076 1.177-3.462 1.718-.004.005-.005.011-.008.016-.005-.006-.007-.013-.012-.02-.386-.54-.896-1.717-3.461-1.717z" fill="#ff7043" fill-rule="evenodd" stroke-width=".3"/></symbol><symbol viewBox="0 0 300.00001 300" id="haskell" xmlns="http://www.w3.org/2000/svg"><g stroke-width="2.422"><path d="M23.928 240.5l59.94-89.852-59.94-89.855h44.955l59.94 89.855-59.94 89.852z" fill="#ef5350"/><path d="M83.869 240.5l59.94-89.852-59.94-89.855h44.955l119.88 179.71h-44.95l-37.46-56.156-37.468 56.156z" fill="#ffa726"/><path d="M228.72 188.08l-19.98-29.953h69.93v29.956h-49.95zm-29.97-44.924l-19.98-29.953h99.901v29.953z" fill="#ffee58"/></g></symbol><symbol viewBox="0 0 210 210" id="haxe" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -87)"><path fill="#f68712" stroke-width=".221" d="M42.78 191.545l63.431-63.43 63.431 63.43-63.431 63.431z"/><path d="M42.8 191.592L31.193 148.28 19.59 104.97 62.9 116.575l43.311 11.605-31.706 31.706z" fill="#fab20b" stroke-width=".266"/><path d="M105.956 128.111l-43.19-11.544-43.177-11.597 22.927.185 23.228.294 20.264 11.36z" fill="#fbc707" stroke-width=".265"/><path d="M19.59 104.97l11.596 43.176 11.545 43.19-11.303-19.948-11.36-20.263-.294-23.228z" fill="#fff200" stroke-width=".265"/><path d="M106.23 128.133l43.312-11.605 43.311-11.605-11.605 43.31-11.605 43.312-31.706-31.706z" fill="#f47216" stroke-width=".266"/><path d="M169.711 191.289l11.545-43.19 11.597-43.176-.185 22.927-.294 23.228-11.36 20.263z" fill="#f1471d" stroke-width=".265"/><path d="M192.853 104.923l-43.176 11.597-43.19 11.544 19.947-11.303 20.264-11.36 23.228-.293z" fill="#fbc707" stroke-width=".265"/><path d="M169.643 191.545l11.605 43.31 11.605 43.312-43.311-11.605-43.311-11.606 31.706-31.705z" fill="#f25c19" stroke-width=".266"/><path d="M106.487 255.025l43.19 11.544 43.176 11.598-22.927-.185-23.228-.294-20.264-11.36z" fill="#f68712" stroke-width=".265"/><path d="M192.853 278.167l-11.597-43.176-11.545-43.19 11.303 19.947 11.36 20.264.294 23.228z" fill="#f1471d" stroke-width=".265"/><path d="M106.211 254.976l-43.31 11.605-43.312 11.605 11.605-43.31L42.8 191.563l31.706 31.706z" fill="#f89c0e" stroke-width=".266"/><path d="M42.731 191.82l-11.545 43.19-11.597 43.176.185-22.927.294-23.228 11.36-20.263z" fill="#fff200" stroke-width=".265"/><path d="M19.59 278.186l43.175-11.597 43.19-11.544-19.947 11.303-20.264 11.36-23.228.293z" fill="#f25c19" stroke-width=".265"/></g></symbol><symbol viewBox="0 0 144 152" id="heroku" xmlns="http://www.w3.org/2000/svg"><path d="M118.68 13.279H26.865c-6.337 0-11.476 5.139-11.476 11.476V129.32c0 6.338 5.139 11.477 11.476 11.477h91.813c6.338 0 11.477-5.14 11.477-11.477V24.755c0-6.337-5.139-11.476-11.477-11.476zM44.08 121.669V96.165l14.346 12.752zm44.632 0v-38.08c-.063-2.976-1.496-6.551-7.97-6.551-12.966 0-27.51 6.52-27.654 6.586l-9.008 4.08V32.407h12.752v36.201c6.366-2.072 15.266-4.321 23.91-4.321 7.882 0 12.6 3.099 15.17 5.698 5.484 5.547 5.56 12.613 5.551 13.43v38.255zm3.188-68.54H79.149c5.011-6.576 8.158-13.496 9.564-20.723h12.751c-.86 7.243-3.796 14.187-9.563 20.722z" fill="#6963b9"/></symbol><symbol viewBox="0 0 24 24" id="hpp" xmlns="http://www.w3.org/2000/svg"><path d="M9.757 19.818H6.751v-5.882q0-2.381-1.737-2.381-.868 0-1.438.663-.56.662-.56 1.718v5.882H0V4.533h3.016v6.508h.037Q4.24 9.239 6.247 9.239q3.51 0 3.51 4.239z" stroke-width=".478" fill="#0277bd"/><path d="M13.073 11.448v2h-2v2h2v2h2v-2h2v-2h-2v-2zm7 0v2h-2v2h2v2h2v-2h2v-2h-2v-2z" fill="#0277bd"/></symbol><symbol viewBox="0 0 24 24" id="html" xmlns="http://www.w3.org/2000/svg"><path d="M12 17.56l4.07-1.13.55-6.1H9.38L9.2 8.3h7.6l.2-1.99H7l.56 6.01h6.89l-.23 2.58-2.22.6-2.22-.6-.14-1.66h-2l.29 3.19L12 17.56M4.07 3h15.86L18.5 19.2 12 21l-6.5-1.8L4.07 3z" fill="#e44d26"/></symbol><symbol viewBox="0 0 24 24" id="http" xmlns="http://www.w3.org/2000/svg"><path d="M16.046 13.784c.074-.613.13-1.225.13-1.856s-.056-1.244-.13-1.856h3.137c.148.594.241 1.215.241 1.856a7.65 7.65 0 0 1-.241 1.856m-4.78 5.16c.557-1.03.984-2.144 1.281-3.304h2.738a7.452 7.452 0 0 1-4.019 3.304m-.232-5.16H9.828a12.314 12.314 0 0 1-.149-1.856c0-.631.056-1.253.149-1.856h4.343c.084.603.149 1.225.149 1.856 0 .63-.065 1.243-.149 1.856M12 19.315c-.77-1.113-1.393-2.348-1.773-3.675h3.545c-.38 1.327-1.002 2.562-1.773 3.675m-3.712-11.1h-2.71a7.353 7.353 0 0 1 4.01-3.304c-.557 1.03-.975 2.144-1.3 3.304m-2.71 7.425h2.71c.325 1.16.743 2.274 1.3 3.304a7.433 7.433 0 0 1-4.01-3.304m-.761-1.856a7.65 7.65 0 0 1-.241-1.856c0-.64.093-1.262.241-1.856h3.137c-.074.612-.13 1.225-.13 1.856 0 .63.056 1.243.13 1.856m4.046-9.253c.77 1.114 1.393 2.357 1.773 3.684h-3.545c.38-1.327 1.002-2.57 1.773-3.684m6.422 3.684h-2.738a14.523 14.523 0 0 0-1.28-3.304 7.412 7.412 0 0 1 4.018 3.304m-6.423-5.568c-5.132 0-9.28 4.176-9.28 9.28a9.28 9.28 0 0 0 9.28 9.282 9.28 9.28 0 0 0 9.281-9.281A9.28 9.28 0 0 0 12 2.647z" fill="#e53935" stroke-width=".928"/></symbol><symbol viewBox="0 0 24 24" id="image" xmlns="http://www.w3.org/2000/svg"><path d="M13.009 9.202h5.368l-5.368-5.368v5.368M6.177 2.37h7.808l5.856 5.856v11.711a1.952 1.952 0 0 1-1.952 1.952H6.178a1.951 1.951 0 0 1-1.952-1.952V4.322c0-1.083.868-1.952 1.952-1.952m0 17.567h11.71V12.13l-3.903 3.903-1.952-1.951-5.856 5.855M8.13 9.202a1.952 1.952 0 0 0-1.952 1.952 1.952 1.952 0 0 0 1.952 1.952 1.952 1.952 0 0 0 1.952-1.952A1.952 1.952 0 0 0 8.13 9.202z" fill="#26a69a" stroke-width=".976"/></symbol><symbol viewBox="0 0 512 512" id="ionic" xmlns="http://www.w3.org/2000/svg"><g fill="#4f8ff7"><path d="M423.592 132.804A31.855 31.855 0 0 0 429 115c0-17.675-14.33-32-32-32a31.853 31.853 0 0 0-17.805 5.409C344.709 63.015 302.11 48 256 48 141.125 48 48 141.125 48 256c0 114.877 93.125 208 208 208 114.873 0 208-93.123 208-208 0-46.111-15.016-88.71-40.408-123.196zM391.83 391.832c-17.646 17.646-38.191 31.499-61.064 41.174-23.672 10.012-48.826 15.089-74.766 15.089-25.94 0-51.095-5.077-74.767-15.089-22.873-9.675-43.417-23.527-61.064-41.174s-31.5-38.191-41.174-61.064C68.982 307.096 63.905 281.94 63.905 256c0-25.94 5.077-51.095 15.089-74.767 9.674-22.873 23.527-43.417 41.174-61.064s38.191-31.5 61.064-41.174c23.673-10.013 48.828-15.09 74.768-15.09 25.939 0 51.094 5.077 74.766 15.089a191.221 191.221 0 0 1 37.802 21.327A31.853 31.853 0 0 0 365 115c0 17.675 14.327 32 32 32 5.293 0 10.28-1.293 14.678-3.568a191.085 191.085 0 0 1 21.327 37.801c10.013 23.672 15.09 48.827 15.09 74.767 0 25.939-5.077 51.096-15.09 74.768-9.675 22.873-23.527 43.418-41.175 61.064z"/><circle cx="256.003" cy="256" r="96"/></g></symbol><symbol viewBox="0 0 24 24" id="java" xmlns="http://www.w3.org/2000/svg"><path d="M2 21h18v-2H2M20 8h-2V5h2m0-2H4v10a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4v-3h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="javascript" xmlns="http://www.w3.org/2000/svg"><path d="M3 3h18v18H3V3m4.73 15.04c.4.85 1.19 1.55 2.54 1.55 1.5 0 2.53-.8 2.53-2.55v-5.78h-1.7V17c0 .86-.35 1.08-.9 1.08-.58 0-.82-.4-1.09-.87l-1.38.83m5.98-.18c.5.98 1.51 1.73 3.09 1.73 1.6 0 2.8-.83 2.8-2.36 0-1.41-.81-2.04-2.25-2.66l-.42-.18c-.73-.31-1.04-.52-1.04-1.02 0-.41.31-.73.81-.73.48 0 .8.21 1.09.73l1.31-.87c-.55-.96-1.33-1.33-2.4-1.33-1.51 0-2.48.96-2.48 2.23 0 1.38.81 2.03 2.03 2.55l.42.18c.78.34 1.24.55 1.24 1.13 0 .48-.45.83-1.15.83-.83 0-1.31-.43-1.67-1.03l-1.38.8z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="javascript-map" xmlns="http://www.w3.org/2000/svg"><path d="M18 8v2h2v10H10v-2H8v4h14V8h-4z" fill="#ffca28"/><path d="M2.444 2.506h14.135v14.136H2.444V2.506m3.714 11.811c.315.668.935 1.218 1.995 1.218 1.178 0 1.987-.629 1.987-2.003V8.993H8.805v4.508c0 .675-.275.848-.707.848-.455 0-.644-.314-.856-.683l-1.084.651m4.697-.14c.392.769 1.185 1.358 2.426 1.358 1.257 0 2.199-.652 2.199-1.854 0-1.107-.636-1.602-1.767-2.089l-.33-.141c-.573-.243-.816-.408-.816-.801 0-.322.243-.573.636-.573.377 0 .628.165.856.573l1.028-.683c-.432-.754-1.044-1.045-1.884-1.045-1.186 0-1.948.754-1.948 1.752 0 1.083.636 1.594 1.594 2.002l.33.141c.613.267.974.432.974.888 0 .377-.354.652-.903.652-.652 0-1.029-.338-1.312-.81l-1.083.63z" fill="#ffca28"/></symbol><symbol viewBox="0 0 180 180" id="jenkins" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="gia"><path transform="scale(1 -1)" fill="#37474f" d="M.899-144.42h144.42V0H.899z"/></clipPath></defs><g transform="matrix(1.0691 0 0 -1.0691 9.4 166.143)" clip-path="url(#gia)"><g fill-rule="evenodd"><path d="M107.96 30.661l-12.506-1.876-16.883-1.876-10.943-.312-10.629.312-8.13 2.502-7.19 7.815-5.628 15.945-1.25 3.44-7.504 2.5-4.377 7.191-3.126 10.317 3.44 9.067 8.128 2.814 6.565-3.127 3.127-6.878 3.752.626 1.25 1.563-1.25 7.19-.313 9.068 1.876 12.505-.074 7.143 5.701 9.114 10.005 7.19 17.508 7.504 19.383-2.814 16.883-12.193 7.817-12.505 5.002-9.067 1.25-22.51-3.752-19.384-6.877-17.195-6.566-9.066" fill="#f0d6b7"/><path d="M97.334-23.425l-44.709-1.876v-7.503l3.752-26.262-1.876-2.19-31.264 10.63-2.19 3.752-3.126 35.328-7.19 21.26-1.563 5.002 25.01 17.195 7.818 3.127 6.877-8.441 5.94-5.315 6.88-2.188 3.125-.938L68.57 1.899l2.814-3.44 7.19 2.502-5.002-9.693 27.2-12.818-3.439-1.876" fill="#335061"/><path d="M23.238 85.687l8.128 2.814 6.566-3.127 3.127-6.878 3.751.626.938 3.751-1.876 7.19 1.876 17.197-1.563 9.379 5.627 6.565 12.193 9.692-3.44 4.69-17.194-8.442-7.191-5.627-4.064-8.754-6.253-8.442-1.876-10.005 1.251-10.63" fill="#6d6b6d"/><path d="M36.055 115.07s4.69 11.567 23.448 17.195c18.759 5.628.938 4.065.938 4.065l-20.321-7.817-7.817-7.816-3.438-6.253 7.19.626M26.676 87.875s-6.566 21.886 18.446 25.012l-.938 3.752-17.195-4.065-5.003-16.257 1.251-10.63 3.439 2.188" fill="#dcd9d8"/></g><g fill="#f7e4cd"><path d="M36.681 58.799l4.094 3.966s1.847-.214 2.16-2.402c.312-2.19 1.25-21.886 14.693-32.516 1.227-.97-10.004 1.564-10.004 1.564L37.62 45.042M94.209 64.739s.729 9.477 3.28 8.748c2.553-.729 2.553-3.28 2.553-3.28s-6.198-4.01-5.833-5.468" fill-rule="evenodd"/><path d="M120.16 99.442s-5.153-1.088-5.628-5.628c-.474-4.54 5.628-.938 6.566-.625M82.327 99.129s-6.879-.938-6.879-5.314c0-4.378 7.817-4.065 10.005-2.19"/><g fill-rule="evenodd"><path d="M39.807 78.808s-11.881 7.191-13.131.312c-1.25-6.877-4.065-11.88 1.876-19.07l-4.064 1.25-3.752 9.691-1.25 9.38 7.19 7.504 8.129-.626 4.69-3.751.312-4.69M45.435 98.504s5.315 27.512 32.203 32.827c22.136 4.375 33.765-.938 38.142-5.94 0 0-19.696 23.447-38.455 16.257-18.759-7.191-32.514-20.322-32.202-28.762.532-14.377.313-14.382.313-14.382M117.97 122.27s-9.066.312-9.38-7.817c0 0 0-1.25.625-2.5 0 0 7.192 8.129 11.568 3.751"/><path d="M78.268 111.1s-1.56 12.477-12.199 5.223c-6.878-4.69-6.252-11.255-5.002-12.505s.91-3.77 1.862-2.04c.952 1.728.638 7.356 4.078 8.918 3.439 1.564 9.077 3.31 11.26.404"/></g></g><g fill="#49728b" fill-rule="evenodd"><path d="M48.874 26.597L19.486 13.466s12.193-48.46 5.94-63.467l-4.377 1.563-.313 18.446-8.128 35.015-3.44 9.692 30.639 20.633 9.067-8.753M51.896-.206l4.17-5.087v-18.76h-5.003s-.625 13.132-.625 14.696c0 1.563.624 7.19.624 7.19M52-26.866l-14.069-.625 4.065-2.813L52-31.868"/></g><g fill-rule="evenodd"><path d="M100.15-23.739l11.567.313 2.814-28.764-11.881-1.563-2.5 30.014" fill="#335061"/><path d="M103.27-23.739l17.508.938s7.19 18.133 7.19 19.07c0 .939 6.253 26.263 6.253 26.263l-14.069 14.694-2.813 2.501-7.504-7.503V3.148l-6.565-26.887" fill="#335061"/><path d="M111.09-21.55l-10.942-2.188 1.563-8.755c4.064-1.876 10.943 3.127 10.943 3.127M111.4 33.162l21.885-16.257.626 7.503-16.57 15.32-5.94-6.566" fill="#49728b"/><path d="M62.85-85.332l-6.473 26.266-3.22 19.38-.531 14.385 29.296 1.56 18.226.003-1.658-32.83 2.814-25.324-.312-4.69-23.76-1.876-14.382 3.126" fill="#fff"/><path d="M96.083-23.426s-1.563-32.515 3.127-55.65c0 0-9.38-5.94-23.136-7.503l26.262.938 3.126 1.875-3.752 51.273-.938 10.944" fill="#dcd9d8"/><path d="M115.06-49.691l12.193 3.44 23.135 1.25 3.44 10.629-6.254 18.446-7.19.938-10.005-3.127-9.599-4.686-5.095.935-3.972-1.56" fill="#fff"/><path d="M114.84-43.435s8.128 3.751 9.38 3.438L120.78-22.8l4.065 1.563s2.814-16.257 2.814-18.133c0 0 17.507-.938 19.07-.938 0 0 3.752 7.191 2.814 14.694l3.44-10.005.312-5.628-5.002-7.503-5.627-1.25-9.38.312-3.126 4.064-10.943-1.563-3.44-1.25" fill="#dcd9d8"/></g><path d="M102.56-21.241L95.682-3.733l-7.19 10.317s1.562 4.377 3.75 4.377h7.192l6.878-2.501-.625-11.568-3.127-18.134" fill="#fff"/><path d="M103.9-15.297S95.145 1.585 95.145 4.086c0 0 1.563 3.752 3.752 2.814 2.19-.938 6.879-3.439 6.879-3.439v5.94l-10.63 2.19-7.19-.939 12.193-28.763 2.5-.313" fill="#dcd9d8" fill-rule="evenodd"/><path d="M65.664 25.968l-8.661.942-8.13 2.501v-2.814l3.972-4.38 12.506-5.627" fill="#fff"/><path d="M51.689 25.031s9.693-4.065 12.819-3.127l.311-3.748-8.752 1.872-5.316 3.752.938 1.251" fill="#dcd9d8" fill-rule="evenodd"/><path d="M115.03 9.897c-5.305.156-10.098.786-14.294 1.97.285 1.72-.249 3.408.18 4.647 1.17.843 3.13.83 4.898 1.027-1.529.752-3.677 1.049-5.44.615-.042 1.194-.578 1.934-.902 2.868 2.982 1.064 10.024 8.044 13.984 5.732 1.887-1.099 2.689-7.377 2.835-10.43.122-2.533-.23-5.088-1.261-6.43" fill="#d33833" fill-rule="evenodd"/><path d="M115.03 9.897c-5.305.156-10.098.786-14.294 1.97.285 1.72-.249 3.408.18 4.647 1.17.843 3.13.83 4.898 1.027-1.529.752-3.677 1.049-5.44.615-.042 1.194-.578 1.934-.902 2.868 2.982 1.064 10.024 8.044 13.984 5.732 1.887-1.099 2.689-7.377 2.835-10.43.122-2.533-.23-5.088-1.261-6.43z" fill="none" stroke="#d33833" stroke-width="2"/><path d="M89.66 18.569c-.014-.401-.03-.806-.047-1.21-1.656-1.089-4.33-1.076-6.148-1.99 2.68-.117 4.79-.763 6.614-1.672l-.118-3.033c-3.036-2.078-5.81-5.173-9.384-7.122-1.69-.922-7.622-3.294-9.42-2.875-1.017.236-1.109 1.499-1.516 2.689-.866 2.548-2.861 6.605-3.035 10.44-.222 4.846-.71 12.967 4.51 11.969 4.213-.804 9.113-2.745 12.375-4.527 1.993-1.09 3.146-2.436 6.17-2.669" fill="#d33833" fill-rule="evenodd"/><path d="M89.66 18.569c-.014-.401-.03-.806-.047-1.21-1.656-1.089-4.33-1.076-6.148-1.99 2.68-.117 4.79-.763 6.614-1.672l-.118-3.033c-3.036-2.078-5.81-5.173-9.384-7.122-1.69-.922-7.622-3.294-9.42-2.875-1.017.236-1.109 1.499-1.516 2.689-.866 2.548-2.861 6.605-3.035 10.44-.222 4.846-.71 12.967 4.51 11.969 4.213-.804 9.113-2.745 12.375-4.527 1.993-1.09 3.146-2.436 6.17-2.669z" fill="none" stroke="#d33833" stroke-width="2"/><path d="M92.675 12.788c-.463 2.64-.999 3.393-.792 5.695 7.04 4.693 8.361-8.061.792-5.695" fill="#d33833" fill-rule="evenodd"/><path d="M92.675 12.788c-.463 2.64-.999 3.393-.792 5.695 7.04 4.693 8.361-8.061.792-5.695z" fill="none" stroke="#d33833" stroke-width="2"/><path d="M102.87 10.649s-2.19 3.127-.626 4.065c1.564.938 3.127 0 4.065 1.563s0 2.501.313 4.377 1.877 2.189 3.44 2.501c1.562.313 5.94.938 6.565-.625l-1.876 5.627-3.752 1.25-11.88-6.877-.626-3.44v-6.877M70.041.331c-.376 4.88-.773 9.752-1.215 14.626-.662 7.279 1.748 6.009 8.057 6.009.964 0 5.933-1.15 6.289-1.876 1.705-3.483-2.851-2.709 1.964-5.335 4.065-2.216 11.246 1.346 9.603 6.273-.919 1.095-4.789.341-6.176 1.06l-7.327 3.8c-3.108 1.612-10.29 3.962-13.603 1.709-8.395-5.71.53-19.974 3.524-25.93" fill="#ef3d3a" fill-rule="evenodd"/><g fill="#231f20" fill-rule="evenodd"><path d="M78.268 111.1c-8.521 1.985-12.755-3.566-15.338-9.323-2.306.559-1.389 3.695-.806 5.294 1.525 4.194 7.672 9.778 12.694 9.02 2.161-.325 5.086-2.301 3.45-4.99M119.79 101.4l.404-.016c1.926-4 3.593-8.238 6.022-11.769-1.628-3.79-12.322-7.144-12.157-.338 2.313 1.01 6.305.206 8.356 1.497-1.186 3.254-2.897 6.024-2.625 10.626M82.63 101.29c1.827-3.35 2.422-6.868 5.019-9.4 1.17-1.14 3.444-2.529 2.316-5.698-.263-.747-2.189-2.414-3.3-2.741-4.06-1.2-13.521-.248-10.317 4.814 3.358-.157 7.871-2.18 10.38.257-1.927 3.081-5.363 9.177-4.098 12.768M118.26 67.253c-6.113-3.927-12.93-8.197-22.947-7.207-2.14 1.86-2.956 6.002-.877 8.737 1.082-1.861.402-5.284 3.419-5.799 5.684-.972 12.299 3.477 16.387 5.032 2.535 4.275-.219 5.847-2.503 8.597-4.675 5.636-10.947 12.622-10.72 21.06 1.89 1.37 2.053-2.092 2.325-2.722 2.44-5.714 8.585-13.021 13.07-17.912 1.1-1.205 2.914-2.36 3.115-3.157.582-2.315-1.513-5.09-1.27-6.63M37.668 71.387c-1.916 1.094-2.372 5.91-4.622 6.048-3.215.195-2.629-6.25-2.616-10.018-2.213 2.009-2.602 8.194-.976 11.37-1.853.91-2.68-1.003-3.708-1.677 1.32 9.595 14.036 4.45 11.922-5.723M122.15 63.257c-2.846-5.417-6.871-11.382-15.222-11.555-.17 1.75-.3 4.411.009 5.464 6.384.614 10.325 3.863 15.212 6.091M82.149 59.745c5.326-2.8 15.114-3.102 22.353-2.89.388-1.586.379-3.545.394-5.48-9.305-.463-20.307 1.84-22.747 8.37M81.136 54.523c3.683-9.247 16.341-8.182 27.016-7.927-.47-1.2-1.489-2.62-2.755-3.132-3.42-1.392-12.855-2.448-17.604.074-3.011 1.601-4.946 5.219-6.596 7.34-.797 1.024-4.765 3.64-.06 3.645"/></g><path d="M117.82 3.516c-4.322-7.402-8.457-15.005-13.585-21.534 2.15 6.32 3.07 16.9 3.394 24.965 4.498 2.105 8.349-.474 10.191-3.43" fill="#81b0c4" fill-rule="evenodd"/><g fill="#231f20" fill-rule="evenodd"><path d="M141.07-23.089c-4.839-.969-8.239-5.671-12.959-5.37 2.594 3.658 7.14 5.2 12.959 5.37M143.21-30.661c-3.944-.417-8.576-1.055-12.577-.726 1.894 2.892 9.19 1.894 12.577.726M144.58-37.19c-4.433-.096-9.942-.008-14.155.346 2.492 2.677 11.28.993 14.155-.346"/></g><g fill-rule="evenodd"><path d="M109.48-55.057c.636-5.567 2.843-11.207 2.566-17.304-2.45-.827-3.858-1.55-7.142-1.545-.232 5.181-.925 13.102-.718 18.041 1.615-.107 3.997 1.154 5.294.808" fill="#dcd9d8"/><path d="M102.33 26.985c-2.226-1.453-4.121-3.267-6.259-4.818-4.74-.235-7.327.328-10.81 3.05.057.219.407.121.42.39 5.075-2.262 11.524.92 16.648 1.378" fill="#f0d6b7"/><path d="M75.694-7.603c1.394 6.04 6.857 9.17 11.817 12.497 5.12-6.498 8.234-14.855 11.663-22.92-8.102 2.443-16.38 6.406-23.481 10.423" fill="#81b0c4"/><path d="M104.18-55.865c-.207-4.94.486-12.86.718-18.041 3.283-.004 4.691.718 7.142 1.545.276 6.096-1.93 11.737-2.566 17.304-1.298.346-3.679-.914-5.294-.808zm-51.13 28.09c2.165-19.906 5.301-36.639 11.054-54.266 12.766-3.876 28.157-4.214 39.441-.716-2.072 9.948-1.167 22.06-2.378 32.677-.912 7.98-.447 16.009-1.698 24.15-13.673 2.844-33 .665-46.418-1.845zm49.651 1.72c-.115-8.549.383-16.982 1.036-25.542 3.282.493 5.51.822 8.56 1.49-.99 8.241-.869 17.514-2.886 24.804-2.332-.023-4.385.027-6.71-.752zm16.653 1.378c-1.558.357-3.372.014-4.86-.015.7-6.969 2.397-14.659 2.995-21.974 2.342-.073 3.593 1.032 5.52 1.403.102 6.421-.562 15.268-3.655 20.586zm25.215-23.038c4.882 1.186 7.952 7.165 6.586 13.305-.916 4.127-2.548 11.898-4.295 14.538-1.29 1.953-4.79 4.51-7.584 2.72-4.545-2.91-12.552-3.755-15.867-7.278 1.662-5.534 2.178-13.135 2.864-20.146 5.678-.354 12.665 1.562 17.387-.471-3.297-1.068-7.575-1.077-10.423-2.633 2.328-1.125 7.778-.897 11.332-.035zM99.17-18.025c-3.43 8.063-6.543 16.42-11.663 22.918-4.96-3.327-10.423-6.456-11.817-12.497 7.1-4.017 15.379-7.98 23.481-10.422zm8.453 24.971c-.325-8.065-1.245-18.644-3.395-24.965 5.128 6.53 9.263 14.132 13.585 21.534-1.842 2.957-5.693 5.536-10.19 3.431zm-9.582 3.405c-1.943.21-3.592-2.233-6.117-1.177-.58-.64-1.105-1.333-1.695-1.958 5.579-6.723 8.114-16.262 12.423-24.163 2.312 7.59 2.045 15.904 2.555 24.188-3.177-.201-4.94 2.873-7.166 3.11zm-6.161 8.132c-.208-2.303.328-3.056.791-5.695 7.57-2.367 6.248 10.388-.791 5.695zm-8.394 2.755c-3.261 1.782-8.161 3.723-12.374 4.527-5.222.999-4.732-7.123-4.51-11.968.173-3.836 2.168-7.893 3.035-10.441.406-1.19.498-2.453 1.515-2.69 1.798-.418 7.73 1.954 9.42 2.875 3.575 1.95 6.348 5.045 9.384 7.123.04 1.011.078 2.021.119 3.032-1.826.91-3.935 1.555-6.615 1.673 1.818.914 4.492.901 6.148 1.989.016.405.033.81.047 1.21-3.024.234-4.176 1.58-6.17 2.67zm-31.152 5.659c-2.707-2.748 7.592-6.494 10.871-6.696-.018 1.739.991 3.378.788 4.626-3.895.684-9.013.232-11.66 2.07zm33.345-1.29c-.013-.27-.363-.172-.42-.39 3.482-2.722 6.07-3.285 10.81-3.05 2.137 1.551 4.033 3.365 6.259 4.818-5.124-.458-11.574-3.64-16.648-1.379zm30.606-9.282c-.146 3.053-.948 9.332-2.835 10.431-3.961 2.312-11.002-4.668-13.984-5.732.324-.934.86-1.674.901-2.868 1.764.434 3.912.137 5.44-.615-1.767-.198-3.727-.185-4.897-1.027-.429-1.239.105-2.927-.18-4.647 4.196-1.184 8.989-1.814 14.294-1.97 1.032 1.341 1.383 3.896 1.261 6.429zM47.777 24.24c-.85.606-6.6 8.087-7.388 7.777-10.405-4.103-20.134-11.199-28.828-17.91 8.29-17.787 11.635-39.579 12.227-60.582 9.496-4.441 17.836-10.844 30.722-11.512-1.491 10.55-2.852 19.962-3.699 29.895-3.237 1.365-7.882-.062-10.913.423-.025 3.651 4.628 1.6 5.015 4.054.292 1.858-2.56 1.998-1.631 4.923 2.368-.861 3.612-2.763 6.138-3.477 2.309 5.05-.032 13.985.3 18.205.064.792.397 4.39 2.172 3.759 1.57-.559-.09-9.569.082-13.563.157-3.68-.444-7.242 1.046-9.552a355.817 355.817 0 0 0 38.576 3.16c-2.964 1.272-6.485 2.475-10.345 4.651-2.093 1.18-8.69 3.635-9.293 5.622-.964 3.167 2.528 4.855 3.125 7.57-6.285-3.428-7.511 3.286-8.998 8.042-1.347 4.308-2.114 7.526-2.445 10.01-5.414 2.581-11.203 5.195-15.863 8.505zm63.009 6.872c8.67 4.204 10.232-15.711 6.834-22.127.525-1.914 2.331-2.646 3.069-4.366-4.838-8.667-10.211-16.756-15.148-25.32 3.672 2.286 8.917.409 13.238 2.12 1.58.624 2.722 4.24 3.918 7.133 3.29 7.958 6.743 17.99 8.28 25.586.346 1.73 1.292 5.5 1.08 7.04-.378 2.758-4.12 4.803-6.022 6.508-3.506 3.15-5.714 5.921-9.371 8.866-1.483-2.189-4.666-3.66-5.878-5.44zM27.95 107.99c-4.13-4.545-3.266-13.062-2.766-19.121 7.467 4.697 17.377-.372 17.284-8.36 3.565.094 1.332 4.452.687 7.259-2.107 9.169 3.55 19.13.256 27.516-6.395-.485-11.649-3.097-15.46-7.294zm29.558 26.38c-9.352-2.65-21.337-9.446-25.18-17.847 2.976.432 5.041 1.933 7.977 2.119 1.11.072 2.563-.466 3.838-.148 2.54.63 4.685 6.327 6.602 8.447 1.868 2.07 4.114 2.954 5.651 4.841.988.477 2.448.444 2.504 1.927-.428.457-.879.806-1.392.66zm48.681-2.493c-9.707 5.477-26.136 9.596-36.462 4.449-8.331-4.155-19.593-11.027-23.433-19.737 3.587-8.405-1.062-16.106-1.36-24.64-.157-4.54 2.139-8.504 2.315-13.446-1.228-2.025-4.978-2.275-7.574-2.136-.873 4.372-2.403 9.287-6.906 9.78-6.371.697-11.03-4.576-11.319-10.085-.342-6.48 4.978-17.22 12.517-16.475 2.913.287 3.629 3.207 6.802 3.177 1.72-3.432-2.653-4.51-3.103-6.964-.117-.634.363-3.112.642-4.274 1.37-5.658 4.422-12.982 7.427-17.29 3.814-5.464 11.307-6.288 19.37-6.823 1.44 3.101 6.743 2.846 10.2 2.035-4.143 1.64-7.993 5.617-11.185 9.137-3.665 4.039-7.378 8.371-7.566 13.65 6.927-9.61 12.65-18.003 25.246-22.23 9.53-3.196 20.662 1.465 27.986 6.608 3.039 2.137 4.853 5.529 7.013 8.634 8.082 11.626 11.854 28.219 11.024 44.303-.342 6.633-.327 13.244-2.552 17.706-2.326 4.666-10.193 8.84-14.8 4.62-.853 4.537 3.83 7.344 9.331 5.71-3.922 5.063-8.039 11.145-13.614 14.29zm18.084-149.66c7.585 3.77 21.757 10.149 26.512-.014 1.755-3.746 3.814-10.079 4.723-13.946 1.284-5.456-1.392-16.923-7-18.754-4.953-1.617-10.733-1.518-16.7-.32-.702.585-1.484 1.603-2.03 2.665-4.261.165-8.25-.229-11.615-1.98.319-3.15-1.812-3.656-3.81-4.305-1.48-5.872 2.963-13.541 1.9-18.896-.76-3.815-5.453-4.405-8.902-5.118-.113-2.12.15-3.89.386-5.683-.789-2.907-4.327-4.561-7.679-4.967-11.029-1.326-27.775-1.922-38.384 1.893-2.96 7.261-5.292 16.093-7.758 24.384-10.346-1.105-18.715 4.464-26.603 8.113-2.731 1.266-6.51 1.964-7.53 4.138-.99 2.105-.584 6.14-.83 9.95-.625 9.733-1.16 19.12-3.73 29.086-1.154 4.472-3.165 8.418-4.568 12.727C9.358 5.184 7.092 10.12 6.5 14.1c-.877 5.903 4.681 6.232 8.235 8.79 5.494 3.954 9.806 6.142 15.756 9.711 1.762 1.057 7.077 3.733 7.681 4.966 1.202 2.443-2.062 5.888-2.935 7.803-1.38 3.03-2.1 5.602-2.298 8.59-4.992.789-8.775 3.76-11.06 7.109-3.781 5.543-6.403 15.798-3.132 23.599.257.614 1.536 1.822 1.725 2.765.372 1.858-.7 4.329-.768 6.305-.343 10.14 1.716 18.875 8.541 21.932 2.771 11.038 12.688 14.71 22.032 20.195 3.493 2.05 7.343 3.36 11.32 4.824 14.263 5.25 36.15 4.261 47.987-4.692 5.02-3.797 13.044-11.813 15.914-17.617 7.58-15.323 7.042-40.931 1.74-59.571-.712-2.503-1.746-6.181-3.19-9.187-1.006-2.1-4.134-6.3-3.754-8.153.391-1.916 7.132-7.034 8.577-8.428 2.603-2.51 7.548-5.843 7.948-9.012.43-3.372-1.485-7.984-2.456-11.238-3.245-10.858-6.412-20.895-10.091-30.576" fill="#231f20"/><path d="M73.674 57.38c.411.548 2.674 1.38 5.84-.144 0 0-3.752-.626-3.44-6.881l-1.564.313s-1.615 5.672-.836 6.712" fill="#f7e4cd"/><path d="M101.09 3.617a1.72 1.72 0 1 0-3.44.001 1.72 1.72 0 0 0 3.44-.001M102.81-4.355a1.72 1.72 0 1 0-3.44 0 1.72 1.72 0 0 0 3.44 0" fill="#1d1919"/></g><g><rect transform="matrix(.8 0 0 -.8 0 144)" x="16.854" y="177.38" width="70.412" height="4.12" rx=".983" ry=".983"/><rect transform="scale(1 -1)" x="78.502" y="-2.097" width="50.037" height="3.296" rx=".786" ry=".786"/><rect transform="scale(1 -1)" x="13.483" y="-3.697" width="54.831" height="3.296" rx=".786" ry=".786"/><rect transform="scale(1 -1)" x="83.296" y="-3.697" width="45.243" height="3.296" rx=".786" ry=".786"/></g></g></symbol><symbol viewBox="0 0 24 24" id="json" xmlns="http://www.w3.org/2000/svg"><path d="M5 3h2v2H5v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5h2v2H5c-1.07-.27-2-.9-2-2v-4a2 2 0 0 0-2-2H0v-2h1a2 2 0 0 0 2-2V5a2 2 0 0 1 2-2m14 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3h2m-7 12a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m-4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#fbc02d"/></symbol><symbol viewBox="0 0 50 50" id="julia" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -247)" stroke-width="5.673"><circle cx="13.497" cy="281.632" r="9.555" fill="#bc342d"/><circle cx="36.081" cy="281.632" r="9.555" fill="#864e9f"/><circle cx="24.722" cy="262.389" r="9.555" fill="#328a22"/></g></symbol><symbol viewBox="0 0 64 64" id="karma" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -233)"><path d="M38.556 288.413l-20.29-26.687 9.532-7.246 20.29 26.686h-.001.002l5.527 7.247z" fill="#359b8b" stroke-width=".173"/><path d="M35.681 241.172L24.92 255.327v-14.13H12.947v13.817l7.84 33.235h4.132v-13.147l.003.003 20.29-26.686-.008-.006 5.504-7.24H35.84v.12z" fill="#3cbeae" stroke-width=".206"/></g></symbol><symbol viewBox="0 0 24 24" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7 14a2 2 0 0 1-2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2 2 2 0 0 1-2 2m5.65-4A5.99 5.99 0 0 0 7 6a6 6 0 0 0-6 6 6 6 0 0 0 6 6 5.99 5.99 0 0 0 5.65-4H17v4h4v-4h2v-4H12.65z" fill="#26a69a"/></symbol><symbol viewBox="0 0 24 24" id="kivy" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(1.89 0 0 1.89 -12.157 -11.429)" fill="#90a4ae"><path d="M7.026 8.63v4.474l1.928-1.928a.437.437 0 0 0 0-.619zM9.38 16.072v-4.474l-1.927 1.927a.437.437 0 0 0 0 .62zM18.576 10.412l-5.346.564-.017.018 2.39 2.39zM9.922 8.502s.023 3.304-.003 4.452c-.02.856.371 1.114.746 1.507.538.564 1.599 1.57 1.599 1.57a.53.53 0 0 0 .75 0l1.843-1.844a.53.53 0 0 0 0-.75z"/></g></symbol><symbol viewBox="0 0 24 24" id="kl" xmlns="http://www.w3.org/2000/svg"><defs><style>.a{fill:#3aaae1}.b{fill:#fdfeff}</style></defs><title>kl</title><path d="M12.033 1.737c-.25-.003-.5.11-.729.337C8.225 5.15 5.15 8.227 2.078 11.31c-.144.144-.229.346-.341.521v.41c.16.223.294.474.485.666a3259.51 3259.51 0 0 0 8.936 8.937c.193.192.443.325.666.486h.41c.205-.142.436-.256.609-.428 3.046-3.041 6.09-6.083 9.133-9.127.47-.47.472-1.005.006-1.472l-9.218-9.217c-.23-.23-.48-.347-.731-.35zm-1.062 4.545l1.386.832c.702.422 1.403.846 2.109 1.262a.544.544 0 0 1 .04.026l.016.013.017.013c.061.056.089.123.088.224a510.281 510.281 0 0 0 0 3.794.463.463 0 0 1-.007.094c-.015.069-.054.103-.142.109a.464.464 0 0 1-.044.002c-.045-.002-.09-.002-.136-.003-.323-.006-.648-.001-.998-.001v-.527-1.34-.671-.003l.004-.668c0-.147-.039-.231-.17-.308-.893-.528-1.78-1.066-2.67-1.6-.051-.03-.101-.065-.173-.111l.001-.003h-.001zm.362 3.39c.068-.003.119.043.173.138.085.148.174.293.264.44l.015.025c.096.154.194.31.292.47l-1.915 1.176c-.337.207-.673.417-1.014.617-.113.067-.154.143-.154.277.01.977.01 1.955.014 2.932V16H7.7V16h-.002c-.004-.053-.014-.112-.014-.17-.005-1.25-.006-2.501-.015-3.751 0-.142.045-.222.164-.294a467.13 467.13 0 0 0 3.353-2.054l.016-.01a.606.606 0 0 1 .032-.017l.016-.008a.308.308 0 0 1 .033-.013l.012-.004a.157.157 0 0 1 .028-.005l.01-.001zm5.677 3.126l.314.54.346.594v.001c-.158.094-.298.178-.438.259l-3.097 1.798c-.106.062-.189.071-.3.01l-.893-.496-1.524-.843-.895-.493c-.035-.02-.068-.044-.129-.085h.001l.137-.25.495-.902 1.446.795c.442.243.886.483 1.323.734.121.07.212.072.334 0 .894-.525 1.792-1.043 2.689-1.563.057-.034.118-.061.191-.1z" fill="#29b6f6" stroke-width=".041"/></symbol><symbol viewBox="0 0 24 24" id="kotlin" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="gpb"><stop offset="0" stop-color="#cb55c0"/><stop offset="1" stop-color="#f28e0e"/></linearGradient><linearGradient id="gpa"><stop offset="0" stop-color="#0296d8"/><stop offset="1" stop-color="#8371d9"/></linearGradient><linearGradient xlink:href="#gpa" id="gpc" x1="1.725" y1="22.67" x2="22.185" y2="1.982" gradientUnits="userSpaceOnUse" gradientTransform="translate(1.638 1.155) scale(.89324)"/><linearGradient xlink:href="#gpb" id="gpd" x1="1.869" y1="22.382" x2="22.798" y2="3.377" gradientUnits="userSpaceOnUse" gradientTransform="translate(1.638 1.155) scale(.89324)"/></defs><path d="M3.307 3.003v18.048h18.05v-.03L16.88 16.51l-4.48-4.515 4.48-4.515 4.443-4.477H3.307z" fill="url(#gpc)"/><path d="M12.538 3.003l-9.23 9.23v8.818h.083l9.032-9.032-.025-.024 4.48-4.515 4.444-4.477h-8.784z" fill="url(#gpd)"/></symbol><symbol viewBox="0 0 240 240" id="laravel" xmlns="http://www.w3.org/2000/svg"><path d="M216.05 119.036c-1.433.343-24.945 6.673-24.945 6.673l-19.227-28.622c-.537-.828-.99-1.656.359-1.849 1.345-.196 23.195-4.477 24.182-4.723.99-.245 1.837-.536 3.053 1.267 1.21 1.8 17.836 24.626 18.464 25.506.627.877-.447 1.41-1.883 1.748m-4.101 49.326c.588 1.003 1.176 1.64-.67 2.367-1.843.73-62.243 22.847-63.418 23.39-1.173.546-2.092.73-3.607-1.637-1.51-2.362-21.16-39.264-21.16-39.264l64.03-18.075c1.876-.644 2.317-.405 3.103.822 1.074 1.68 21.143 31.403 21.726 32.4m-103.7-21.087c-.78.202-37.566 9.733-39.525 10.22-1.965.485-1.965.246-2.188-.49-.226-.727-43.728-98.053-44.333-99.271-.605-1.214-.574-2.177 0-2.177.571 0 34.734-3.313 35.944-3.383 1.207-.07 1.08.205 1.526 1.033l49.025 91.818c.84 1.58 1.239 1.81-.452 2.248m94.588-59.77c-3.5-4.58-5.2-3.751-7.357-3.41-2.154.336-27.277 4.915-30.194 5.449-2.918.536-4.758 1.803-2.963 4.53 1.597 2.422 18.113 27.824 21.751 33.42l-65.663 17.066L66.18 49.832c-2.075-3.342-2.507-4.514-7.236-4.28-4.735.23-40.969 3.495-43.55 3.731-2.58.233-5.416 1.479-2.835 8.09 2.583 6.612 43.734 102.82 44.88 105.62 1.149 2.803 4.128 7.345 11.11 5.527 7.157-1.871 31.969-8.894 45.52-12.742 7.163 14.07 21.77 42.619 24.473 46.707 3.607 5.459 6.089 4.56 11.626 2.738 4.325-1.42 67.65-26.129 70.502-27.4 2.855-1.273 4.613-2.184 2.685-5.275-1.419-2.28-18.124-26.558-26.876-39.26 5.993-1.733 27.305-7.888 29.575-8.557 2.646-.779 3.008-2.19 1.572-3.94-1.436-1.755-21.293-28.72-24.79-33.296z" fill="#ff5722" stroke="#ff5722" stroke-width="8.852" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 24 24" id="less" xmlns="http://www.w3.org/2000/svg"><path d="M13.696 2.999V5h2.002v5a2 2 0 0 0 1.999 2 2 2 0 0 0-2 2v5h-2v2h2a2 2 0 0 0 2-2v-4a2 2 0 0 1 2-2h1V11h-1a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2.001zm-.03 12.766v.47a1 1 0 0 0 .03-.236 1 1 0 0 0-.03-.234zM10.566 21v-2.001H8.565v-5a2 2 0 0 0-2-2 2 2 0 0 0 2-2V5h2.001v-2H8.565a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-.999V13h1a2 2 0 0 1 2 2v3.999A2 2 0 0 0 8.564 21zm.03-12.766v-.47a1 1 0 0 0-.03.236 1 1 0 0 0 .03.234z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="lib" xmlns="http://www.w3.org/2000/svg"><path d="M19 7H9V5h10m-4 10H9v-2h6m4-2H9V9h10m1-7H8a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2M4 6H2v14a2 2 0 0 0 2 2h14v-2H4V6z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 40 40" id="livescript" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -257)" fill="#317eac"><path stroke-width="3.299" d="M5.419 260.18h3.685v34.207H5.419z"/><path stroke-width="3.299" d="M37.074 288.197v3.685H2.867v-3.685z"/><path stroke-width="2.894" d="M29.612 265.658l2.004 2.005L7.428 291.85l-2.004-2.005z"/><path stroke-width="2.325" d="M10.73 262.471h2.835v22.08H10.73z"/><path stroke-width="2.063" d="M15.36 262.519h2.835v17.382H15.36z"/><path stroke-width="1.77" d="M19.99 262.471h2.835v12.802H19.99z"/><path stroke-width="1.422" d="M24.526 262.491h2.835v8.254h-2.835z"/><path stroke-width="1.128" d="M28.783 262.463h2.835v5.197h-2.835z"/><path stroke-width="2.325" d="M34.801 286.545v-2.835h-22.08v2.835z"/><path stroke-width="2.063" d="M34.753 281.914v-2.835H17.371v2.835z"/><path stroke-width="1.77" d="M34.801 277.284v-2.835H21.999v2.835z"/><path stroke-width="1.422" d="M34.781 272.749v-2.835h-8.254v2.835z"/><path stroke-width="1.128" d="M34.809 268.492v-2.835h-5.197v2.835z"/></g></symbol><symbol viewBox="0 0 24 24" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M12 17a2 2 0 0 0 2-2 2 2 0 0 0-2-2 2 2 0 0 0-2 2 2 2 0 0 0 2 2m6-9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2h1V6a5 5 0 0 1 5-5 5 5 0 0 1 5 5v2h1m-6-5a3 3 0 0 0-3 3v2h6V6a3 3 0 0 0-3-3z" fill="#ffd54f"/></symbol><symbol viewBox="0 0 24 24" id="lua" xmlns="http://www.w3.org/2000/svg"><circle cx="12.203" cy="12.102" r="10.322" fill="none" stroke="#42a5f5"/><path d="M12.33 5.746a6.483 6.381 0 0 0-6.482 6.381 6.483 6.381 0 0 0 6.482 6.38 6.483 6.381 0 0 0 6.484-6.38 6.483 6.381 0 0 0-6.484-6.38zm1.86 1.916a2.329 2.292 0 0 1 2.33 2.293 2.329 2.292 0 0 1-2.33 2.291 2.329 2.292 0 0 1-2.329-2.29 2.329 2.292 0 0 1 2.328-2.294z" fill="#42a5f5" fill-rule="evenodd"/><ellipse cy="4.615" cx="19.631" rx="2.329" ry="2.292" fill="#42a5f5" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 24 24" id="markdown" xmlns="http://www.w3.org/2000/svg"><path d="M2 16V8h2l3 3 3-3h2v8h-2v-5.17l-3 3-3-3V16H2m14-8h3v4h2.5l-4 4.5-4-4.5H16V8z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" preserveAspectRatio="xMidYMid" id="markojs" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -120.96)" stroke-width=".984"><path d="M4.002 126.482c-.655 1.07-1.32 2.14-1.976 3.21-.655 1.06-1.308 2.142-1.963 3.212l.002.002-.002.002c.655 1.07 1.308 2.15 1.963 3.211.655 1.07 1.32 2.141 1.976 3.211h3.33c-.664-1.07-1.318-2.14-1.974-3.21-.653-1.069-1.307-2.145-1.961-3.214.654-1.068 1.308-2.146 1.961-3.215a601.93 601.93 0 0 1 1.974-3.209z" fill="#2196f3"/><path d="M3.999 126.482l-.002.002c.655 1.07 1.31 2.15 1.964 3.212.655 1.07 1.32 2.14 1.974 3.21h3.331c-.664-1.07-1.319-2.14-1.974-3.21-.653-1.068-1.306-2.146-1.96-3.214z" fill="#26a69a"/><path d="M15.203 126.482l.002.002c-.655 1.07-1.31 2.15-1.965 3.212-.655 1.07-1.319 2.14-1.974 3.21h-3.33c.664-1.07 1.318-2.14 1.973-3.21.654-1.069 1.307-2.146 1.961-3.214z" fill="#8bc34a"/><path d="M11.874 126.484c.664 1.07 1.318 2.14 1.974 3.21.653 1.068 1.307 2.146 1.961 3.214-.654 1.069-1.308 2.145-1.961 3.213-.656 1.07-1.31 2.14-1.974 3.21h3.33c.655-1.07 1.319-2.14 1.974-3.21.655-1.06 1.31-2.14 1.966-3.21l-.002-.003.002-.002c-.656-1.07-1.311-2.152-1.966-3.213-.655-1.07-1.319-2.138-1.974-3.209z" fill="#ffc107"/><path d="M16.74 126.482c.665 1.07 1.319 2.14 1.974 3.21.654 1.068 1.306 2.146 1.96 3.214-.654 1.069-1.306 2.145-1.96 3.213-.655 1.07-1.31 2.141-1.974 3.211h3.33c.656-1.07 1.32-2.14 1.974-3.21.655-1.062 1.31-2.141 1.966-3.212l-.002-.002.002-.002c-.655-1.07-1.31-2.152-1.966-3.213-.655-1.07-1.318-2.138-1.973-3.209z" fill="#f44336"/></g></symbol><symbol viewBox="0 0 23 24" id="mathematica" xmlns="http://www.w3.org/2000/svg"><path d="M11.512 1.523l-.073.025-.46.794-.454.763-1.217 2.09H9.29L5.435 3.5l-.1-.047h-.018v.092l.025.163v.086l.132 1.226v.082l.032.252v.082l.22 2.137v.075l.018.082v.06l-2.348.507-.04.015-.457.1-.025.01h-.042l-1.096.244-.04.007-.17.036v.082l.018.01 1.859 2.086.053.052.114.132.804.909v.005l-.053.05-.22.257-2.564 2.875-.01.007v.082l.071.006.295.075 1.697.366v.006l2.139.472h.015v.047l-.036.252v.08l-.046.412v.082l-.036.244v.082l-.045.412v.08l-.05.41v.08l-.036.244v.082l-.046.412v.082l-.05.407v.082l-.032.248V20l-.05.407v.104h.037l3.642-1.6.294-.134h.018l.177.312.539.911.015.032.854 1.465.16.262.404.695.007.022h.092l.005-.022.017-.025.56-.947.014-.042.6-1.033.316-.539.644-1.091.05.013 3.906 1.721h.035v-.085l-.138-1.32v-.082l-.032-.244v-.082l-.035-.245v-.085l-.033-.244v-.081l-.032-.245v-.082l-.032-.244v-.085l-.035-.245v-.082l-.032-.245v-.082l-.033-.244v-.085l-.025-.17v-.053l1.632-.354.043-.008.458-.107h.028v-.01l.23-.05.03-.01h.042l.382-.09.025-.01h.043l.194-.05h.033l1.015-.23.07-.007v-.064l-.015-.013-1.19-1.342-.028-.028-.197-.22-1.428-1.604v-.006l.295-.323.4-.457 2.148-2.408.015-.01v-.065l-.035-.008-1.288-.28-.372-.084-.047-.01-2.481-.544v-.045l.432-4.265v-.02h-.042l-.302.135-.01.014h-.025l-3.307 1.45-.297.135h-.015l-2.028-3.483-.099-.145-.014-.045zm-.001 1.114l1.365 2.323.34.592-.008.025-1.18 1.511-.517.66-.012-.01-.258-.335-.04-.05-1.397-1.787.03-.063 1.378-2.365.287-.491zm4.908 2.039l-.007.025-.168.225-.538.066zm-9.817.004l.053.02.677.3h-.499l-.224-.3zM16.947 5l-.123 1.248-.113-.928.226-.307zm-9.26.156l.053.024.705.309-.757-.175zm7.388.116l.02.168-1.318.403.003-.003.16-.071 1.015-.444zM9.669 6.388l.944 1.204v.01L9.483 7.2zm3.55.172l.21.682-.234.084-.089.022-.702.255.008-.022.776-.982zm-5 .836l.986.356.898.312.048.02 1.054.373.011 3.086-.362-.117-.67-.224-.081-.038-.735-.245-.77-.256-.29-.1-.011-.255-.032-1.195-.01-.287-.015-.894-.013-.297zm6.583 0l-.011.227-.028.9-.008.303-.032 1.475-.01.262-.337.117-.734.245-.77.256-.712.245-.355.117.01-3.086 1.632-.578zm.585.437l.09.735.79-.097-.915 1.302-.018.006.01-.183.018-.877zm-9.451.536l.152.22 1.447 2.049-2.607.968-.05.015-1.972-2.214-.28-.312.003-.01.115-.018.424-.1.14-.021.337-.078.042-.01zm11.146.003l3.284.713.029.01-.022.025-1.954 2.192-.277.312-.092-.036-2.564-.95.475-.681.152-.216zM6.787 8.52h.86l.036 1.258-.013-.006-.763-1.078zm1.358 2.625l.152.06.77.252.712.245.746.247.49.167-.065.092-1.723 2.334-1.015-.302-.082-.017-.035-.015-1.902-.56.938-1.22.981-1.277zm6.73 0l.033.006 1.787 2.327.132.17-.128.036-.032.014-2.196.642-.105.032-.564.17-.018-.003-1.053-1.44-.174-.239-.547-.726-.007-.018.469-.16.769-.254.713-.245.77-.252zm-7.766.305l-.007.02-.405.523-.291-.291.657-.245zm8.802 0l.043.007.578.212.714.27-.661.394-.375-.479-.03-.042-.262-.342zm-10.843.75l-.67.668.355-.397.207-.23zm12.911.016l.068.025.045.042.554.627.042.043.204.228-.255.135zm-6.473.265l.022.015 1.38 1.872.032.05.343.465.008.031-.088.117-.422.629-.047.074-.245.343-.97 1.43-.013.007-1.18-1.72-.096-.16-.493-.708-.008-.037 1.618-2.191.007-.01zm7.827 1.194l.565.633.063.082-.272-.093-.037-.013zm-15.785.148l.297.299-.637.218-.152.05.038-.058zm13.224.47l-.855.448.346.66-.185-.058-.27-.088-1.092-.348.012-.01zm-9.687.255l1.222.356-.006.007-.458.145-.443.135-.032.01-.49.157zm-2.765.048l.318.32 2.007.517-.567.18-.055.004-2.103-.469-.744-.156.007-.006zm14.966.205l.548.188v.003l-.457.1-.043.014-1.069.23zm-10.23.507l.007.227.01.347.025 1.363.025.691-.007.255-.24.107-2.863 1.255.032-.372.033-.255.017-.227.031-.256.037-.407.045-.42.018-.23.032-.251.032-.412.05-.414.013-.14 1.455-.457.003-.014.301-.098zm4.908 0l1.245.39v.014l.312.1 1.146.362.022.23.03.255.043.408.04.42.017.23.033.251.032.412.042.325.078.848-.078-.04-3.025-1.322-.004-.305.06-2.368zm-4.295.617l.015.007.067.107.6.875-.64.531-.034-1.438zm3.671 0h.008l-.005.06-.02.678-.005.214-.479-.223zm-2.888 3.605l.763.915.001.37-.017-.006-.025-.05-.464-.791-.012-.018zm1.53.61l.184.083-.343.586-.018.007.002-.532z" fill="#f44336" fill-rule="evenodd" stroke="#f44336" stroke-width=".7747499999999999" stroke-linejoin="round"/></symbol><symbol viewBox="0 0 720 720" id="matlab" xmlns="http://www.w3.org/2000/svg"><title>Layer 1</title><path d="M209.247 329.98L52.368 387.638l121.325 85.822 96.752-95.804-61.198-47.674z" fill="#4db6ac" fill-rule="evenodd" stroke-width=".3"/><path d="M480.193 71.446c-13.123 1.784-9.565 1.013-28.4 16.09-18.008 14.418-69.925 100.347-97.673 129.256-24.688 25.722-34.46 12.199-60.102 33.661-25.68 21.494-65.273 64.464-65.273 64.464l63.978 47.32L394.15 222.754c23.948-32.932 23.694-37.266 36.744-71.82 6.384-16.907 17.76-29.9 27.756-45.809 12.488-19.874 30.186-34.855 21.543-33.68z" fill="#00897b" fill-rule="evenodd" stroke-width=".3"/><path d="M478.206 69.796c-31.268-.189-62.068 137.245-115.56 242.691-54.543 107.519-162.235 176.82-162.235 176.82 18.156 8.243 34.681 4.91 54.236 23.394 13.375 16.164 52.09 95.976 75.174 146.117 0 0 18.964-10.297 42.994-27.695 24.03-17.397 53.124-41.896 73.384-70.3 26.883-37.692 47.897-61.043 65.703-75.271 17.806-14.23 32.404-19.336 46.458-20.54 50.238-4.305 124.582 85.792 124.582 85.792S527.267 70.09 478.206 69.796z" fill="#ffb74d" fill-rule="evenodd" stroke-width=".3"/></symbol><symbol viewBox="0 0 24 24" id="merlin" xmlns="http://www.w3.org/2000/svg"><text style="line-height:1.25;-inkscape-font-specification:'Century Gothic Bold'" x="1.953" y="21.178" transform="scale(.99582 1.0042)" font-weight="700" font-size="30.255" font-family="Century Gothic" letter-spacing="0" word-spacing="0" fill="#42a5f5" stroke-width=".756"><tspan x="1.953" y="21.178" style="-inkscape-font-specification:'Century Gothic Bold'" font-size="22.745">M</tspan></text></symbol><symbol viewBox="0 0 192 191.99999" id="mocha" xmlns="http://www.w3.org/2000/svg"><title>Mocha Logo</title><g transform="translate(-354.75 -262.42) scale(4.835)" fill="#a1887f"><path d="M103.6 69.6c0-.5-.4-1-1-1H83.8c-.5 0-1 .4-1 1 0 3.4.5 15.1 5.5 20.8.2.2.4.3.7.3h8.4c.3 0 .5-.1.7-.3 5-5.6 5.5-17.3 5.5-20.8zm-7.4 18.2h-5.9c-.3 0-.5-.1-.7-.3-3.4-4-3.8-12-3.9-14.8 0-.5.4-1 1-1h13.2c.5 0 1 .4 1 1 0 2.8-.5 10.7-3.9 14.8-.3.2-.5.3-.8.3zM95.1 66.6s3.6-2.1 1.4-5.9c-1.3-2-1.9-3.7-1.4-4.4-1.3 1.6-3.5 3.3-1.1 6.9.8.9 1.2 2.8 1.1 3.4zM91.1 66.9s2.4-1.4.9-4c-.9-1.3-1.3-2.5-.9-2.9-.9 1.1-2.3 2.2-.7 4.7.5.5.7 1.8.7 2.2z"/><path d="M99.3 78.5c-.4 2.7-1.2 5.8-2.9 7.8-.2.2-.4.3-.6.3h-5c-.2 0-.5-.1-.6-.3-1.2-1.5-2-3.5-2.5-5.6 0 0 5.8.8 9.1-.4 2.4-.9 2.5-1.8 2.5-1.8z"/></g></symbol><symbol viewBox="0 0 24 24" id="movie" xmlns="http://www.w3.org/2000/svg"><path d="M18 4l2 4h-3l-2-4h-2l2 4h-3l-2-4H8l2 4H7L5 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V4h-4z" fill="#ff9800"/></symbol><symbol viewBox="0 0 24 24" id="music" xmlns="http://www.w3.org/2000/svg"><path d="M16 9V7h-4v5.5c-.42-.31-.93-.5-1.5-.5A2.5 2.5 0 0 0 8 14.5a2.5 2.5 0 0 0 2.5 2.5 2.5 2.5 0 0 0 2.5-2.5V9h3m-4-7a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z" fill="#ef5350"/></symbol><symbol viewBox="0 0 24 24" id="mxml" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m.12 13.5l3.74 3.74 1.42-1.41-2.33-2.33 2.33-2.33-1.42-1.41-3.74 3.74m11.16 0l-3.74-3.74-1.42 1.41 2.33 2.33-2.33 2.33 1.42 1.41 3.74-3.74z" fill="#ffa726"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-actions" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#ab47bc" stroke-width="12.914"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-effects" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#26c6da" stroke-width="12.914"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-reducer" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#e53935" stroke-width="12.914"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-state" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#9ccc65" stroke-width="12.914"/></symbol><symbol viewBox="0 0 24 24" id="nim" xmlns="http://www.w3.org/2000/svg"><path d="M4.464 15.75L2.288 3.78l5.985 7.617L12.08 3.78l3.809 7.617 5.985-7.617-2.177 11.97H4.464m15.234 3.264a1.088 1.088 0 0 1-1.088 1.088H5.553a1.088 1.088 0 0 1-1.089-1.088v-1.089h15.234z" stroke-width="1.088" fill="#ffca28"/></symbol><symbol viewBox="0 0 500 500" id="nix" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-1.965 36.302)" stroke-width=".395"><path d="M135.59 415.7c0-.295-2.752-5.283-6.116-11.084-3.364-5.801-6.116-10.776-6.116-11.055s9.514-16.889 21.143-36.912c11.629-20.022 21.323-36.798 21.542-37.279.346-.76-1.608-4.363-14.896-27.466-8.412-14.625-15.294-26.785-15.294-27.023 0-.5 24.46-43.501 25.206-44.31.414-.45.592-.384 1.078.395.32.513 16.876 29.256 36.791 63.87 62.62 108.85 74.852 130.01 75.41 130.46.3.242.544.554.544.694 0 .14-11.836.21-26.302.154-23.023-.09-26.313-.175-26.393-.694-.11-.714-27.662-48.825-28.86-50.392-.746-.978-.906-1.035-1.426-.51-.688.696-28.954 49.323-29.49 50.733l-.365.96h-13.229c-10.896 0-13.229-.095-13.229-.538zm167.58-125.61c-.134-.216 1.188-2.863 2.938-5.882 6.924-11.944 84.291-145.75 96.491-166.88 7.143-12.371 13.142-22.465 13.333-22.433.363.062 25.861 43.105 25.861 43.655 0 .174-6.761 11.952-15.026 26.173-8.46 14.557-14.932 26.104-14.81 26.421.185.483 4.564.564 30.213.564h29.996l.958 1.48c.526.814 3.296 5.547 6.155 10.518 2.859 4.971 5.45 9.29 5.756 9.597.706.705.704.724-.16 1.572-.395.388-3.36 5.323-6.587 10.965-3.228 5.643-6.056 10.387-6.285 10.543-.23.156-19.695.171-43.256.034l-42.84-.249-.804 1.15c-.441.632-7.504 12.736-15.696 26.897l-14.892 25.747H339.03c-8.517 0-20.015.116-25.55.259-6.55.168-10.15.121-10.309-.135zM169.42 132.23c-56.373-.055-102.5-.182-102.5-.282 0-.1 5.617-10.132 12.481-22.294l12.481-22.112h30.332c27.113 0 30.332-.065 30.332-.611 0-.336-6.659-12.228-14.797-26.427-8.139-14.199-14.797-25.917-14.797-26.04 0-.123 2.682-4.853 5.96-10.51s6.003-10.578 6.055-10.934c.086-.586 1.376-.648 13.572-.648 7.413 0 13.463.143 13.446.317-.017.174.222.707.531 1.184.31.476 9.763 16.937 21.007 36.578 11.244 19.64 20.71 36.022 21.036 36.4.554.647 2.549.691 31.428.691h30.837l12.896 22.145c7.093 12.18 12.8 22.301 12.682 22.492-.118.19-4.776.303-10.352.249-5.575-.054-56.26-.143-112.63-.198z" fill="#5075c1"/><path d="M25.289 203.14c-6.098 10.563-6.69 11.711-6.225 12.078.283.224 3.18 5.044 6.44 10.712 3.261 5.668 6.017 10.355 6.124 10.417.106.061 13.585.153 29.95.204 16.367.052 29.994.23 30.285.399.472.273-1.08 3.094-14.637 26.574L62.06 289.793l12.907 21.865c7.1 12.026 12.982 21.906 13.068 21.956.086.05 23.257-39.831 51.492-88.624 11.352-19.617 21.214-36.64 30.37-52.442 23.308-40.452 30.68-53.468 30.73-54.132-1.097-.11-6.141-.187-13.006-.216-3.945-.01-7.82-.02-12.75-.002l-25.341.092-15.42 26.706c-14.256 24.693-15.445 26.663-16.278 26.86l-.024.037c-.011.003-1.62-.001-1.825 0-4.29.062-20.453.063-40.226-.01-22.632-.082-41.615-.125-42.183-.096-.568.03-1.147-.03-1.29-.132-.142-.102-3.29 5.066-6.996 11.485zm205.16-190.3c-.123.149 5.62 10.392 12.761 22.763 12.199 21.131 89.393 155.03 96.276 167 1.502 2.613 2.92 4.803 3.443 5.348.9-1.249 3.531-5.63 7.954-13.219a1342.88 1342.88 0 0 1 10.049-17.76l6.606-11.443c.692-1.403.754-1.818.653-2.117-.162-.48-6.904-12.332-14.982-26.337-8.078-14.005-14.824-25.849-14.991-26.32a.73.73 0 0 1-.009-.366l-.426-.913L359.42 72.5c3.69-6.307 6.425-11.042 9.47-16.29 9.159-15.948 12.037-21.189 11.896-21.55-.126-.324-2.7-4.83-5.72-10.017-3.021-5.185-5.845-10.148-6.275-11.026-.483-.987-.734-1.364-1.1-1.456-.054.014-.083.018-.145.035-.42.112-5.454.195-11.189.185-5.734-.01-11.22.024-12.188.073l-1.76.089-14.997 25.978c-12.824 22.212-15.084 25.964-15.595 25.883-.024-.004-.15-.189-.235-.301-.109.066-.2.09-.272.05-.255-.148-7.143-11.902-15.306-26.119l-14.36-25.016c-.115-.186-.444-.744-.457-.752-.477-.275-50.502.287-50.737.57zm-18.646 283.09c-.047.109-.026.262.042.48.329 1.05 25.338 43.735 25.772 43.985.207.119 14.178.239 31.05.266 26.651.044 30.75.152 31.234.832.308.43 9.988 17.214 21.513 37.296s21.152 36.627 21.394 36.767c.242.14 5.927.243 12.633.23 6.706-.013 12.401.099 12.657.246.132.076.382-.141.852-.795l6.008-10.406c5.234-9.065 6.62-11.684 6.294-11.888-.575-.36-15.597-26.643-23.859-41.482-3.09-5.45-5.37-9.516-5.441-9.774-.195-.712-.065-.822 1.156-.98 1.956-.252 57.397-.057 58.07.205.238.092.79-.569 2.594-3.497 1.866-3.067 5.03-8.524 11-18.866 7.22-12.505 13.044-22.784 12.942-22.843-.102-.059-.771-.051-1.489.016l-.046.001c-4.452.204-33.918.203-149.74.025-38.96-.06-69.786-.09-71.912-.072-1.121.01-2.095.076-2.66.172a.25.25 0 0 0-.062.083z" fill="#7db7e1"/></g></symbol><symbol viewBox="0 0 24 24" id="nodejs" xmlns="http://www.w3.org/2000/svg"><path d="M12 1.85c-.27 0-.55.07-.78.2l-7.44 4.3c-.48.28-.78.8-.78 1.36v8.58c0 .56.3 1.08.78 1.36l1.95 1.12c.95.46 1.27.47 1.71.47 1.4 0 2.21-.85 2.21-2.33V8.44c0-.12-.1-.22-.22-.22H8.5c-.13 0-.23.1-.23.22v8.47c0 .66-.68 1.31-1.77.76L4.45 16.5a.26.26 0 0 1-.11-.21V7.71c0-.09.04-.17.11-.21l7.44-4.29c.06-.04.16-.04.22 0l7.44 4.29c.07.04.11.12.11.21v8.58c0 .08-.04.16-.11.21l-7.44 4.29c-.06.04-.16.04-.23 0L10 19.65c-.08-.03-.16-.04-.21-.01-.53.3-.63.36-1.12.51-.12.04-.31.11.07.32l2.48 1.47c.24.14.5.21.78.21s.54-.07.78-.21l7.44-4.29c.48-.28.78-.8.78-1.36V7.71c0-.56-.3-1.08-.78-1.36l-7.44-4.3c-.23-.13-.5-.2-.78-.2M14 8c-2.12 0-3.39.89-3.39 2.39 0 1.61 1.26 2.08 3.3 2.28 2.43.24 2.62.6 2.62 1.08 0 .83-.67 1.18-2.23 1.18-1.98 0-2.4-.49-2.55-1.47a.226.226 0 0 0-.22-.18h-.96c-.12 0-.21.09-.21.22 0 1.24.68 2.74 3.94 2.74 2.35 0 3.7-.93 3.7-2.55 0-1.61-1.08-2.03-3.37-2.34-2.31-.3-2.54-.46-2.54-1 0-.45.2-1.05 1.91-1.05 1.5 0 2.09.33 2.32 1.36.02.1.11.17.21.17h.97c.05 0 .11-.02.15-.07.04-.04.07-.1.05-.16C17.56 8.82 16.38 8 14 8z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 300 300" id="nodemon" xmlns="http://www.w3.org/2000/svg"><title>nodemon</title><path d="M149.868 20.62c-2.124 0-4.25.55-6.154 1.648L41.899 81.083a12.306 12.306 0 0 0-6.15 10.652v117.633a12.29 12.29 0 0 0 6.152 10.646l101.815 58.766h.001a12.282 12.282 0 0 0 12.291 0l101.84-58.766a12.29 12.29 0 0 0 6.153-10.652V91.738a12.31 12.31 0 0 0-6.146-10.652L156.015 22.27a12.302 12.302 0 0 0-6.153-1.648zM83.303 70.93s11.789 33.031 35.477 31.934l27.74-15.961a7.348 7.348 0 0 1 3.414-.99h.641a7.233 7.233 0 0 1 3.404.99l27.738 15.961c23.69 1.094 35.475-31.934 35.475-31.934 5.233 23.154 1.06 38.641-5.924 48.942l4.541 2.614h.002c2.321 1.327 3.734 3.795 3.737 6.49l-.12 95.811a3.724 3.724 0 0 1-1.855 3.227 3.624 3.624 0 0 1-3.735 0L177.1 206.971c-2.311-1.363-3.742-3.818-3.742-6.48v-44.763a7.44 7.44 0 0 0-3.737-6.465l-15.642-9.01a7.28 7.28 0 0 0-3.715-1.01 7.378 7.378 0 0 0-3.742 1.01l-15.648 9.01c-2.316 1.323-3.729 3.798-3.729 6.467v44.762c0 2.663-1.413 5.1-3.738 6.48l-36.748 21.041a3.571 3.571 0 0 1-3.71 0c-1.173-.65-1.864-1.887-1.864-3.224l-.137-95.812a7.483 7.483 0 0 1 3.74-6.49l4.541-2.615c-6.982-10.302-11.16-25.79-5.925-48.942z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 990 990" id="npm" xmlns="http://www.w3.org/2000/svg"><defs><style>.hncls-1{fill:#cb3837}.cls-2{fill:#fff}</style></defs><title>n</title><path class="hncls-1" d="M113.26 876.74V113.27h763.47v763.47zm143.59-620.4v476.18h240.61V355.63h140.21v376.96h95.457V256.34z" fill="#e53935" stroke-width=".771"/></symbol><symbol id="nunjucks" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.host0{fill:#388e3c}</style><path class="host0" d="M11.2 21.1H8.1l-2.3-7.9v7.9H2.7V2.9h3.1l2.3 7.4V2.9h3.1zM21.3 19.2c0 1-.8 1.9-1.9 1.9h-4.8c-1 0-1.9-.8-1.9-1.9v-3.8l3.2-.7V18h2.3V7.2h3.1v12z"/></symbol><symbol viewBox="0 0 150 150.00001" id="ocaml" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.76136 0 0 .76136 11.616 19.98)"><path d="M83.02 101.645l.023-.062c-.035-.159-.047-.195-.024.062z" fill="none" stroke-width="1.028"/><linearGradient id="hpa" gradientUnits="userSpaceOnUse" x1="-696.735" y1="97.7" x2="-696.735" y2="142.997" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><path d="M82.313 138.79c-.471-1.004-1.904-3.621-2.624-4.46-1.562-1.828-1.927-1.966-2.386-4.275-.799-4.02-2.913-11.31-5.405-16.341-1.286-2.596-3.426-4.777-5.385-6.66-1.71-1.652-5.565-4.431-6.237-4.294-6.296 1.257-8.249 7.432-11.21 12.323-1.638 2.705-3.374 5.007-4.665 7.885-1.192 2.646-1.087 5.577-3.128 7.849-2.093 2.333-3.454 4.814-4.48 7.829-.194.574-.747 6.596-1.348 8.015l9.357-.659c8.719.594 6.2 3.936 19.81 3.208l21.487-.665c-.666-1.97-1.584-4.25-1.938-4.991-.599-1.248-1.352-3.69-1.848-4.763z" fill="url(#hpa)" stroke-width="1.028"/><linearGradient id="hpb" gradientUnits="userSpaceOnUse" x1="-666.972" y1="142.12" x2="-666.972" y2="142.12" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><linearGradient id="hpc" gradientUnits="userSpaceOnUse" x1="-675.228" y1="-1.28" x2="-675.228" y2="142.967" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><path d="M109.553 94.296c-1.652 1.193-4.88 4.06-11.902 5.145-3.152.487-6.1.527-9.335.365-1.584-.076-3.077-.157-4.665-.177-.936-.008-4.074-.107-3.919.193l-.349.871c.054.287.169 1.004.2 1.177.129.704.165 1.265.192 1.912.048 1.331-.11 2.719-.043 4.062.141 2.787 1.175 5.326 1.306 8.137.143 3.13 1.69 6.442 3.188 8.998.569.973 1.434 1.084 1.811 2.283.442 1.373.024 2.83.239 4.293.842 5.675 2.477 11.606 5.032 16.728.018.043.038.09.06.128 3.156-.53 6.318-1.665 10.418-2.271 7.517-1.115 17.972-.54 24.688-1.17 16.993-1.597 26.216 6.97 41.478 3.459V22.459c0-11.84-9.594-21.438-21.435-21.438H19.239C7.4 1.021-2.197 10.62-2.197 22.458v46.774c3.067-1.11 7.479-7.635 8.861-9.222 2.419-2.775 2.858-6.315 4.062-8.544 2.743-5.078 3.215-8.57 9.451-8.57 2.907 0 4.061.67 6.027 3.31 1.368 1.834 3.731 5.224 4.837 7.49 1.277 2.615 3.357 6.153 4.272 6.867.677.53 1.35.928 1.976 1.163 1.012.38 1.848-.316 2.525-.855.863-.687 1.235-2.088 2.035-3.957 1.152-2.696 2.408-5.926 3.122-7.054 1.237-1.949 1.658-4.261 2.993-5.381 1.97-1.652 4.54-1.768 5.246-1.908 3.957-.781 5.755 1.906 7.704 3.645 1.276 1.138 3.019 3.432 4.256 6.507.967 2.4 2.199 4.622 2.714 6.008.497 1.339 1.725 3.484 2.453 6.055.661 2.336 2.43 4.125 3.102 5.235 0 0 1.029 2.882 7.285 5.516 1.357.572 4.1 1.501 5.736 2.096 2.718.988 5.351.86 8.704.458 2.391 0 3.686-3.462 4.772-6.234.643-1.639 1.259-6.334 1.678-7.667.406-1.297-.544-2.3.265-3.437.946-1.327 1.508-1.399 2.054-3.129 1.172-3.704 7.95-3.89 11.761-3.89 3.176 0 2.772 3.083 8.16 2.028 3.086-.605 6.059.398 9.335 1.265 2.758.732 5.352 1.566 6.906 3.385 1.005 1.178 3.5 7.08.958 7.331.244.3.423.84.88 1.135-.566 2.226-3.03.64-4.4.355-1.845-.383-3.147.057-4.952.856-3.085 1.374-7.598 1.214-10.286 3.452-2.281 1.898-2.277 6.133-3.34 8.507-.002-.001-2.955 7.6-9.402 12.248z" fill="url(#hpc)" stroke-width="1.028"/><linearGradient id="hpd" gradientUnits="userSpaceOnUse" x1="-735.137" y1="90.833" x2="-735.137" y2="141.967" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><path d="M38.247 105.09c-1.467-.15-2.83-.317-4.256-.605-2.662-.536-5.57-1.06-8.193-1.688-1.592-.385-6.895-2.263-8.048-2.792-2.702-1.246-4.496-4.63-6.609-4.282-1.348.22-2.662.682-3.5 2.042-.685 1.11-.917 3.016-1.391 4.294-.55 1.485-1.5 2.87-2.331 4.284-1.53 2.595-4.282 4.941-5.468 7.469-.239.52-.45 1.101-.649 1.708V144.415a48.57 48.57 0 0 1 4.45.96c11.955 3.19 14.872 3.46 26.598 2.119l1.1-.146c.897-1.867 1.59-8.227 2.171-10.195.454-1.51 1.077-2.712 1.313-4.253.223-1.463-.02-2.858-.146-4.188-.329-3.332 2.427-4.522 3.742-7.384 1.186-2.589 1.871-5.535 2.853-8.181.941-2.54 2.41-6.13 4.918-7.408-.305-.355-5.237-.518-6.554-.65z" fill="url(#hpd)" stroke-width="1.028"/></g></symbol><symbol viewBox="0 0 24 24" id="pdf" xmlns="http://www.w3.org/2000/svg"><path d="M14 9h5.5L14 3.5V9M7 2h8l6 6v12a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m4.93 10.44c.41.9.93 1.64 1.53 2.15l.41.32c-.87.16-2.07.44-3.34.93l-.11.04.5-1.04c.45-.87.78-1.66 1.01-2.4m6.48 3.81c.18-.18.27-.41.28-.66.03-.2-.02-.39-.12-.55-.29-.47-1.04-.69-2.28-.69l-1.29.07-.87-.58c-.63-.52-1.2-1.43-1.6-2.56l.04-.14c.33-1.33.64-2.94-.02-3.6a.853.853 0 0 0-.61-.24h-.24c-.37 0-.7.39-.79.77-.37 1.33-.15 2.06.22 3.27v.01c-.25.88-.57 1.9-1.08 2.93l-.96 1.8-.89.49c-1.2.75-1.77 1.59-1.88 2.12-.04.19-.02.36.05.54l.03.05.48.31.44.11c.81 0 1.73-.95 2.97-3.07l.18-.07c1.03-.33 2.31-.56 4.03-.75 1.03.51 2.24.74 3 .74.44 0 .74-.11.91-.3m-.41-.71l.09.11c-.01.1-.04.11-.09.13h-.04l-.19.02c-.46 0-1.17-.19-1.9-.51.09-.1.13-.1.23-.1 1.4 0 1.8.25 1.9.35M8.83 17c-.65 1.19-1.24 1.85-1.69 2 .05-.38.5-1.04 1.21-1.69l.48-.31m3.02-6.91c-.23-.9-.24-1.63-.07-2.05l.07-.12.15.05c.17.24.19.56.09 1.1l-.03.16-.16.82-.05.04z" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="perl" xmlns="http://www.w3.org/2000/svg"><path d="M12 14c-1 0-3 1-3 2 0 2 3 2 3 2v-1a1 1 0 0 1-1-1 1 1 0 0 1 1-1v-1m0 5s-4-.5-4-2.5c0-3 3-3.75 4-3.75V11.5c-1 0-5 1.5-5 4.5 0 4 5 4 5 4v-1M10.07 7.03l1.19.53c.43-2.44 1.58-4.06 1.58-4.06-.43 1.03-.71 1.88-.89 2.55C13.16 3.55 15.61 2 15.61 2a15.916 15.916 0 0 0-2.64 3.53c1.58-1.68 3.77-2.78 3.77-2.78-2.69 1.72-3.9 4.45-4.2 5.21l.55.08c0 .52 0 1 .25 1.38C14.1 11.31 18 11.47 18 16s-4.03 6-6.17 6C9.69 22 5 21.03 5 16s4.95-5.07 5.83-7.08c.12-.38-.76-1.89-.76-1.89z" fill="#9575cd"/></symbol><symbol viewBox="0 0 24 24" id="php" xmlns="http://www.w3.org/2000/svg"><path d="M12 18.08c-6.63 0-12-2.72-12-6.08s5.37-6.08 12-6.08S24 8.64 24 12s-5.37 6.08-12 6.08m-5.19-7.95c.54 0 .91.1 1.09.31.18.2.22.56.13 1.03-.1.53-.29.87-.58 1.09-.28.22-.71.33-1.29.33h-.87l.53-2.76h.99m-3.5 5.55h1.44l.34-1.75h1.23c.54 0 .98-.06 1.33-.17.35-.12.67-.31.96-.58.24-.22.43-.46.58-.73.15-.26.26-.56.31-.88.16-.78.05-1.39-.33-1.82-.39-.44-.99-.65-1.82-.65H4.59l-1.28 6.58m7.25-8.33l-1.28 6.58h1.42l.74-3.77h1.14c.36 0 .6.06.71.18.11.12.13.34.07.66l-.57 2.93h1.45l.59-3.07c.13-.62.03-1.07-.27-1.36-.3-.27-.85-.4-1.65-.4h-1.27L12 7.35h-1.44M18 10.13c.55 0 .91.1 1.09.31.18.2.22.56.13 1.03-.1.53-.29.87-.57 1.09-.29.22-.72.33-1.3.33h-.85l.5-2.76h1m-3.5 5.55h1.44l.34-1.75h1.22c.55 0 1-.06 1.35-.17.35-.12.65-.31.95-.58.24-.22.44-.46.58-.73.15-.26.26-.56.32-.88.15-.78.04-1.39-.34-1.82-.36-.44-.99-.65-1.82-.65h-2.75l-1.29 6.58z" fill="#1E88E5"/></symbol><symbol viewBox="0 0 79 78" id="postcss" xmlns="http://www.w3.org/2000/svg"><title>postcss-logo-symbol</title><g transform="translate(5.48 5.52) scale(.85425)" fill="#e53935" fill-rule="evenodd" stroke="#e53935" stroke-width="1.519"><path d="M15.447 32.623c.106.08.29.132.106.29-.132.184-.29.342-.395.553-.105.185-.184.237-.342.106.21-.343.42-.66.63-.95zM68.342 60.24c0 .078.026.13.026.21.053-.105.053-.158.08-.21zm0 .236v-.026zm-5.368 10.277l-4.58-25.402c-.078-.025-.183-.077-.368-.13.053.105.08.184.106.263.13-.026.184-.026.236-.052 0-.026 0-.052.027-.08l4.58 25.404zm-4.737-31.12c-.026.078-.026.158-.026.237 0-.08 0-.16.028-.238zm.026.526c-.026 0-.026 0-.052-.028v.026c.028.026.028.026.054 0zm-.052.21v-.185c-.077.026-.156.026-.262.053.132.05.264.078.264.13z"/><path d="M78.71 33.967c-.052-1.028-.078-2.056-.184-3.083-.184-1.397-.368-2.82-.684-4.19-.237-1.133-.63-2.214-1.026-3.294-.5-1.265-1-2.556-1.632-3.768-1.026-1.95-2.368-3.69-3.605-5.508-.818-1.16-1.87-2.108-2.66-3.294-.447-.685-1.105-1.264-1.763-1.79-1.053-.845-2.158-1.61-3.263-2.347a32.525 32.525 0 0 0-2.58-1.634c-.71-.397-1.473-.713-2.21-1.056-.842-.395-1.658-.87-2.605-1.054-.238-.05-.448-.13-.685-.21-.605-.21-1.184-.447-1.79-.632-.92-.29-1.815-.632-2.763-.87C50.342 1 49.394.843 48.446.71 47.394.555 46.316.5 45.262.397a26.83 26.83 0 0 0-2.026-.184C42.236.16 41.21.16 40.21.134c-.5-.027-1.026-.08-1.526-.053-.763.026-1.526.105-2.29.21-.736.08-1.473.21-2.183.317-.867.105-1.735.158-2.604.264-.816.106-1.658.264-2.473.396-.29.053-.58.158-.87.21-.63.132-1.288.185-1.92.396-1.13.344-2.263.74-3.368 1.16-1.027.422-2.027.87-3 1.397-1 .552-1.948 1.21-2.895 1.844a45.325 45.325 0 0 0-2.66 1.923c-.84.66-1.63 1.397-2.394 2.135-.42.42-.763.922-1.158 1.396-.657.765-1.315 1.502-1.947 2.293-.524.66-1 1.344-1.5 2.03-.893 1.21-1.656 2.502-2.366 3.794-.29.527-.553 1.054-.816 1.58-.395.79-.816 1.555-1.184 2.372-.264.554-.474 1.16-.632 1.766-.367 1.292-.736 2.61-1.078 3.9-.316 1.16-.395 2.372-.42 3.558-.027 1.054.078 2.082.183 3.136.027.264-.13.58.184.79-.105.29-.026.45.13.5-.182.29.08.476-.024.74-.027.052.08.157.13.236 0 .08-.025.185 0 .264.028.237.133.474.133.738 0 .184.157.395.21.58.026.078 0 .21-.053.263-.158.184-.132.342.105.448.133.342.08.5.054.66.052.236-.027.315 0 .368.21.422.29.896.315 1.37 0 .106.053.212.106.343.026 0 0 .5 0 .5.13-.078.237-.104.368-.157.08.342.158.66.263.95.132.21.132.314.08.34.105.474.157.922.34 1.37 0-.5-.05-1-.13-1.475.368.132.684.263.895.263.027-.08.053-.184.08-.237-.158-.157-.29-.394-.448-.552.053.21 0 .29 0 .37-.105-.054-.237-.107-.368-.16.105-.13.21-.263.368-.42 0-.238-.13-.45-.5-.423.158-.052.316-.13.5-.184.29-.157-.026-.447-.026-.816.026-.447-.237-.895-.316-1.37-.132-.737-.105-1.844-.184-2.582-.158-.132-.29.21-.316.237.08.632.158 1.264.21 1.897-.157-.527-.263-1.107-.394-1.74-.027.185-.053.264-.053.37-.13.13-.026.29.053.474-.184-.08-.395-.052-.395-.052v.738c-.262-.264-.34-.474-.473-.66-.052-.21-.08-.42-.13-.63.05-.133 0-.212 0-.29a15.968 15.968 0 0 1-.08-.634c.026-.026-.026-.42-.026-.42.21.025.343.05.474.05-.263-.34-.08-.552.027-.763.053-.106.237-.13.29-.238.21-.395.553-.71.553-1.212 0-.237.08-.5.105-.738.053-.448.105-.896.13-1.344.054-.58 0-1.16.133-1.713.212-.92.475-1.843.764-2.766.21-.66.448-1.29.71-1.95.395-1.028.764-2.056 1.264-3.03.71-1.424 1.526-2.794 2.316-4.19.5-.87 1.026-1.687 1.58-2.53.525-.817 1.05-1.66 1.657-2.425a21.452 21.452 0 0 1 2.79-2.978c1.053-.948 2.053-1.923 3.184-2.793a32.218 32.218 0 0 1 4.685-3.005c1.343-.71 2.737-1.266 4.132-1.793.895-.342 1.868-.5 2.79-.79 1.052-.343 2.105-.5 3.21-.527.71-.027 1.395-.106 2.105-.185.632-.05 1.263-.104 1.948-.183-.08.105-.106.158-.132.21-.288.422-.604.844-.894 1.265-.237.343-.5.712-.737 1.054-.422.555-.87 1.108-1.264 1.688-.605.87-1.158 1.766-1.79 2.635-.63.843-1.315 1.634-1.973 2.45-.868 1.134-1.684 2.293-2.552 3.426-.79 1.08-1.63 2.11-2.394 3.19-.684.947-1.29 1.95-1.948 2.923-.973 1.45-1.947 2.872-2.92 4.322a271.93 271.93 0 0 1-2.316 3.294c-.053.08-.132.104-.21.157-.21.342-.21.527-.29.685-.21.395-.42.79-.658 1.16-.132.21-.316.394-.474.605-.026-.316.42-.474.21-.87-.13.212-.263.396-.394.607l-.316.63c.105.08.29.133.105.29-.08.133-.158.29-.237.423a.954.954 0 0 0 .29-.264c0 .29-.158.526-.29.763-.105.21-.368.37-.552.527.026.027.21.106.237.132.237-.08.316-.21.343-.132.08-.105.158-.184.184-.263.104-.264.262-.474.525-.58.106-.053.184-.132.263-.21.79-.818 1.606-1.608 2.316-2.478 1.106-1.345 2.106-2.74 3.16-4.11.446-.58.973-1.16 1.446-1.714.078.606.026 1.185 0 1.74-.08.974-.132 1.95-.21 2.95-.027.395 0 .79-.027 1.186 0 .105-.08.184-.08.29 0 .263.08.553.08.817-.08.975-.186 1.923-.265 2.898-.027.21.078.422.13.607-.13 1.422.16 2.925-.078 4.427.184-.29.237-.474.237-.658.025-.158 0-.316 0-.5v-.264c.025-.475.13-.975.078-1.45-.053-.527-.053-1.027.053-1.528.053-.21-.026-.474.106-.738v.395c-.026 1.5.027 3.003-.183 4.505-.027.132.08.37-.21.343-.238.474.052.817-.21 1.08-.054.053.05.29.077.448-.106.317-.106.317.052.343.026.58.08 1.106.105 1.66.42-1 .21-2.03.396-3.058.026.422.053.844.026 1.29 0 .687-.026 1.345-.052 2.03 0 .132-.027.264-.053.396-.08.37-.105.738-.237 1.08-.105.264-.052.66-.052.975v1.003c.105.448-.027.685.052.948-.08.265-.105.344-.08.423l.08.395c.527-.053.29.343.5.553-.158.212-.105.29-.105.397 0 .237-.025.448-.052.685 0 .606-.026 1.212-.026 1.792 0 .08.026.157.026.236 0 .054-.026.74-.026.74.053.078 0 .157-.08.236-.025 0-.104-3.347-.104-3.347h-.395c-.052 1.58.08 3.003-.21 4.48-.316.025-.42.078-.764.078-.816 0-1.632 0-2.448.026-.974 0-1.92.026-2.895.026-.472 0-.972.054-1.446.054-.632 0-1.29-.08-1.92-.08-.975 0-1.922.08-2.896.106-.71.026-1.42.026-2.13.053-.475.025-.95.05-1.422.104-.21.026-.395.105-.658.184-.08 0-.263-.026-.42 0-.265.053-.5.21-.765.264-.395.08-.5.184-.448.58v.263c-.026.052.58-.08.58-.08-.054 0-.08.158-.16.29.212-.08.343-.132.475-.184.395.185.737.08 1.052.16 1.026.262 2.078.37 3.13.473.685.053 1.343.08 2.027.105.973.053 1.947.106 2.92.106.816 0 1.606-.08 2.42-.08 1.13 0 2.264.052 3.395.08.237 0 .5-.028.763-.028h1.92c1.712-.052 3.422-.08 5.133-.13.975-.028 1.975-.08 2.948-.107l3-.08c1.158-.026 2.316-.026 3.448-.05.868 0 1.71-.03 2.58-.055.972-.026 1.972-.105 2.946-.157.527-.027 1.054-.08 1.58-.132.632-.052 1.29-.13 1.92-.157.948-.054 1.922-.08 2.87-.133 1.184-.078 2.368-.183 3.578-.21 1.106-.052 2.237-.026 3.343-.052.974-.027 1.948-.08 2.948-.106l1.66-.08s1.104-.026 1.657-.08c.947-.052 1.894-.157 2.842-.183.604-.027 1.21 0 1.815-.027.973-.026 1.973-.08 2.947-.08.367 0 .762.054 1.236.08-.21.185-.342.29-.5.422.105.026.21.08.316.132a.71.71 0 0 1-.42.13c-.054.133-.107.186-.16.45h.474c-.184 0-.342.237-.526.395-.21-.054-.395 0-.5.29.184.104.158.183.132.29-.316.104-.553.21-.42.552-.107.052-.238.105-.37.184-.13.21-.368.263-.316.553.106.025.21.08.29.104-.132.053-.263.132-.395.184-.473.29-.262.422-.157.554-.08.053-.158.105-.237.132.052.237.13.29.157.29a9.3 9.3 0 0 0-.395.316c-.08.237-.185.342-.29.5s-.158.37-.29.527c-.552.607-.947 1.32-1.657 1.793-.264.185-.5.422-.737.66-.474.447-.895.948-1.395 1.37a29.595 29.595 0 0 1-2.052 1.554 151.56 151.56 0 0 1-2.604 1.792c-.474.315-1 .552-1.5.842s-.974.554-1.474.843c-.316.21-.606.5-.948.66-.868.37-1.79.685-2.684 1.028-.87.37-1.5.685-2.158.922-.605.21-1.237.37-1.868.5-.21.054-.448 0-.685.027-.448.08-.895.186-1.343.238-1.158.158-2.316.264-3.473.422-.685.08-1.343.21-2.027.29-.473.026-.973-.026-1.447-.026-.342 0-.71.08-1.053.027-.552-.08-1.105-.21-1.658-.316-.13-.026-.316-.08-.42-.026-.21.106-.396-.052-.607 0-.13.027-.262-.08-.394-.08-.106-.025-.238.028-.37 0-.29-.078-.552-.183-.87-.157-.313.026-.63-.132-.97-.21-.475-.106-.92-.21-1.396-.317a2.38 2.38 0 0 1-.525-.237c-.685 0-1.133-.026-1.554-.185-.368-.13-.71-.315-1.105-.262-.104.026-.183-.026-.29-.026-.08-.106-.157-.317-.235-.317-.526.027-.842-.42-1.29-.553-.236-.08-.42-.343-.657-.422-.58-.237-1.052-.737-1.71-.816-.21-.027-.42-.132-.658-.21.08.104.13.183.21.262-.763-.37-1.473-.79-2.184-1.186-.104-.026-.183-.13-.262-.184l-.71-.474c-.395.08-.553-.08-.66-.132-.71-.5-1.525-.817-2.21-1.37-.29-.238-.63-.396-.84-.686-.37-.448-.817-.764-1.317-1.027-.394-.21-.762-.448-1.13-.685-.185-.132-.37-.29-.37-.58 0-.185-.078-.37-.315-.264-.105-.158-.21-.342-.342-.395-.316-.13-.526-.37-.763-.58s-.42-.5-.71-.605c-.527-.21-.843-.658-1.158-1.027-.738-.87-1.396-1.82-2.08-2.74-.053-.08-.158-.133-.237-.212.105.29.237.527.368.79-.262-.105-.446-.29-.604-.474-.027.027 1.815 3.057 1.815 3.057.16.237.29.475.448.712a.813.813 0 0 1-.79-.422c-.236-.42-.5-.684-1.026-.63a4.588 4.588 0 0 1-.13-.58c-.107 0-.185 0-.37-.027.37.58.685 1.08 1.027 1.66-.133-.08-.21-.132-.265-.158.473.5.815 1.133 1.42 1.45.132.605.816.895.974 1.475-.13-.027-.238-.053-.37-.08-.21-.263-.447-.526-.683-.816.052.184.13.342.236.474.316.395.606.79.974 1.133.132.134.316.187.316.424.21.105.29.13.368.13.054.16-.025.397.29.344.21.395.42.395.71.264.343.343.528.37.764.16 0 .13.026.262.026.368.105-.053.08-.132.08-.264.13.105.21.158.262.21.263.37.5.712.868 1.002.5.422.948.87 1.42 1.265.922.765 1.95 1.398 2.975 1.977 1.264.712 2.475 1.476 3.764 2.16 1.552.818 3.21 1.372 4.92 1.767.632.132 1.237.263 1.87.42.55.16 1.104.397 1.657.528.842.185 1.71.343 2.552.5.183.027.37.054.58.08.235.053.524-.053.577.027.132.21.237.104.395.078.184-.053.395-.053.605-.053.737.026 1.447.184 2.184.132.16 0 .396-.133.528.13.236-.105.368-.105.473-.13.028.236 0 .236-.05.262-.054.026-.133.053-.238.132.947.184 1.842.21 2.63 0 1.37.105 2.554-.053 3.686-.448.105.132.184.316.342.053.052-.08.184-.107.29-.133.236-.053.526-.158.736-.08.238.08.317-.13.5-.13.317 0 .606-.027.896-.08.158-.026.316-.105.5-.158a1.285 1.285 0 0 0-.58-.133c.317-.158.606-.29.896-.42-.053.078-.106.183-.21.183h.367c-.08 0-.185.237-.316.395.946-.237 1.814-.448 2.657-.66-.29-.552.315-.367.526-.684-.263.08-.526.158-.79.21.895-.447 1.816-.842 2.71-1.237-.13.158-.29.237-.525.37.158.025.263.025.342.05.42.133.316-.262.447-.5.5 0 .71-.078.947-.158.263-.08.526-.158.79-.263.42-.184.815-.42 1.236-.63.08-.028.21 0 .316 0 .29-.186.394-.344.473-.318.37.053.63-.08.736-.42.184-.133.316-.238.447-.318.578-.316 1.13-.632 1.71-.948.21 0 .316 0 .368-.027.344-.16.66-.342.975-.527a2.258 2.258 0 0 1-.263-.13c.262-.054.34-.08.5-.133.63-.74 1.5-1.24 2.157-1.82.29-.026.29-.105.29-.157.104-.132.21-.29.34-.396.58-.527 1.21-.975 1.737-1.528a37.16 37.16 0 0 0 2.184-2.374c.63-.738 1.264-1.475 1.79-2.292.737-1.133 1.368-2.293 2.026-3.48.474-.842.895-1.685 1.37-2.528.05-.08.157-.185.236-.185.71-.08 1.422-.13 2.106-.21.158-.026.342-.13.5-.21-.08-.132-.132-.29-.21-.422-.106-.16-.264-.29-.37-.45-.104-.13-.183-.29-.262-.447-.08-.13-.158-.236-.237-.37a9.7 9.7 0 0 1-.45-.894c-.026-.08-.08-.21-.052-.29.474-1.027.658-2.134 1.105-3.162.447-1.054.58-2.24.79-3.373.184-1.08.29-2.16.42-3.24.08-.764.185-1.502.21-2.266.16-1.212.106-2.346.08-3.48-.026-1-.08-2.028-.13-3.03zM12.685 66.405c-.184-.21-.342-.448-.526-.658l.08-.08c.287.317.577.633.866.976-.158-.08-.342-.132-.42-.238zm.42.238c.08-.027.16-.027.238-.053.08.132.132.29.21.448-.368-.027-.552-.185-.447-.395zm27.37 10.883v-.08c.5-.052.973-.105 1.473-.157v.077c-.5.08-.973.13-1.473.158zm6.63-.685c-.367.08-.762.133-1.13.186-.132.026-.29.158-.342-.08-.053.027-.106.027-.158.054.13.394.447.078.71.236-.58.08-1.13.132-1.684.21v-.052c.16-.026.343-.053.5-.08v-.078a7.743 7.743 0 0 0-.79-.053c-.077 0-.183.106-.262.132-.105.026-.21.053-.342.053-.447.026-.894.026-1.316.052-.027 0-.08-.026-.106-.026v-.08c1.763-.236 3.5-.473 5.263-.71.027.052.027.105.053.157-.158 0-.263.055-.395.08zm.396-.262c.606-.08 1.16-.132 1.738-.21-1.21.342-1.605.394-1.737.21zM24.58 23.374c.84-1.16 1.71-2.32 2.552-3.505.263-.345.473-.714.736-1.056.08-.106.185-.158.316-.264l-.026-.05c.105-.133.21-.24.263-.344.134-.21.213-.448.318-.685a.385.385 0 0 1 .105-.103c.37.184.37-.21.5-.343.237-.264.474-.553.684-.817.158-.21.316-.395.448-.632.026-.08-.053-.21-.08-.317h-.078c.08-.052.158-.13.237-.184.026 0 .026 0 .052-.026.158-.238.316-.475.474-.686.315-.42.657-.842 1.025-1.21-.052.13-.105.263-.158.368.027 0 .027.027.053.027.316-.422.658-.817.974-1.24-.027-.025-.053-.052-.08-.052-.13.132-.236.264-.368.396-.026-.027-.052-.053-.08-.053.265-.343.528-.685.79-1.08.053.08.106.184.21.395.107-.263.212-.447.29-.632-.078.08-.183.158-.262.238l-.08-.08.474-.71c.5-.712 1-1.45 1.5-2.162.185-.263.42-.474.58-.738.5-1 1.29-1.792 1.894-2.714.132-.184.316-.342.474-.5.13-.16.237-.106.342.026.71.896 1.42 1.818 2.13 2.714.528.66 1.054 1.29 1.554 1.976.605.844 1.184 1.687 1.79 2.53.684.975 1.368 1.95 2.026 2.95 1 1.477 1.947 2.953 2.947 4.428.737 1.08 1.474 2.135 2.184 3.215h-1.344c-1.236-.025-2.5-.13-3.736-.078-1.684.08-3.394.264-5.078.396-2.132.185-4.29.21-6.42.21-.765 0-1.528.107-2.29.16-.922.052-1.817.105-2.738.13-1.08.054-2.13.08-3.21.107-.606.026-1.237 0-1.895 0zm30.183 12.12v.238c-.026 0-.052.027-.105.027-.105-.37-.21-.766-.342-1.135-.263-.765-.553-1.53-1.027-2.214-.528-.737-1-1.5-1.528-2.265-.13-.185-.316-.343-.474-.5-.553-.607-1.106-1.24-1.816-1.687a21.485 21.485 0 0 0-3.29-1.688 7.374 7.374 0 0 1-.92-.474h.63l4.5-.08c.974-.025 1.922-.025 2.895-.078.236 0 .368.08.5.29.236.395.473.79.736 1.186.027.052.08.13.08.21 0 .58 0 1.186.026 1.766.025.606.08 1.186.104 1.792 0 .606-.053 1.238-.026 1.87.027.897.053 1.82.053 2.74zM26.447 26.67c1.237-.053 2.42-.132 3.632-.185.945-.053 1.92-.08 2.866-.132.395-.025.764-.05 1.158 0-.42.212-.842.423-1.21.686-.474.316-.92.737-1.395 1.08-.475.342-.896.764-1.29 1.212-.5.605-1.053 1.132-1.58 1.712-.37.422-.79.817-1.105 1.265-.447.58-.842 1.21-1.263 1.87.132-2.504.29-4.98.184-7.51zm17.185 25.35c-.843.21-1.71.448-2.58.553-.736.106-1.5.08-2.263.08a25.42 25.42 0 0 1-2.028-.08c-.763-.078-1.526-.157-2.263-.5-.633-.29-1.29-.553-1.92-.87-.634-.316-1.265-.684-1.74-1.264-.34-.423-.815-.765-1.236-1.134.08.316.263.58.553.764-.132.158-.316.08-.58-.343-.078.053-.157.08-.21.106.08-.185.158-.37.237-.527-.105-.21-.237-.448-.342-.66-.21-.342-.42-.71-.605-1.053-.053-.08-.053-.158-.105-.237a5.893 5.893 0 0 1-.37-.475c-.21-.315-.394-.657-.657-.974 0 .08.027.158.027.264-.027 0-.053.026-.053.026l-.554-1.344c-.026 0-.026 0-.052.026l.473 1.74c-.026 0-.052.025-.08.025-.077-.104-.156-.21-.21-.34-.052-.212-.21-.212-.34-.133-.08.053-.133.237-.106.316.185.448.395.896.606 1.344.052.158.105.29.184.448.027.053.106.105.106.184.106.21.185.42.316.606.237.316.5.632.737.948.235.316.445.66.656.975.026.053.105.053.13.08.133.395.58.684.896.526.08.606.737.817 1 1.397a11.957 11.957 0 0 1-.763-.343c-.027.026-.027.052-.054.105.316.158.632.316.92.5.265.16.528.317.765.5.316.29.685.45 1.13.554a.282.282 0 0 0-.05-.107c.736.343 1.5.712 2.078 1-2.737.054-5.658.107-8.685.16 0-.5-.026-.975-.026-1.476 0-.21.052-.395.025-.606-.08-1.21-.08-2.424-.237-3.61-.157-1.264-.157-2.503-.13-3.77.025-.683-.027-1.394-.054-2.08 0-.922 0-1.82.028-2.74 0-.132.053-.237.106-.37h.08c.025.054 0 .133.05.16.08.08.212.21.265.184.157-.106.394-.21.447-.37.13-.315.184-.658.184-.974 0-.236.106-.394.21-.553.054-.08.08-.158.133-.263-.105-.08-.21-.132-.342-.237.106-.29.08-.633.475-.79.052-.027.052-.16.08-.238.025-.213.05-.45.078-.66.052.08.08.105.13.157a.42.42 0 0 1 .054-.08c0-.104-.026-.315 0-.315.316-.053.184-.395.342-.553.025-.028-.027-.107-.027-.16 0-.052 0-.13.026-.13.367-.08.315-.475.552-.66.08-.053.105-.13.21-.263.21.368-.158.553-.184.816.446-.263.578-.895.315-1.08.105-.08.21-.184.29-.29.29-.316.604-.606.868-.922.185-.236.29-.526.474-.763.106-.132.316-.237.474-.317.474-.262.92-.552 1.21-1 .053-.053.132-.105.21-.158.08-.053.238-.053.264-.132.027-.052-.052-.184-.105-.263.104-.053.21-.158.42-.264-.08.158-.105.264-.158.37l.13.13c.238-.184.606-.394.843-.552 0-.025-.132-.13-.132-.13-.157.08-.394.21-.63.316.05-.08.05-.132.08-.158.367-.237.735-.474 1.13-.66.92-.42 1.842-.842 2.763-1.237.158-.08.37-.026.553-.026.078 0 .13 0 .21-.026.42-.132.842-.264 1.263-.37.183-.052.393-.078.58-.078.787.025 1.577.025 2.366.078.342.026.658.105.974.21a9.88 9.88 0 0 1 1.184.5c.447.24.868.502 1.29.792.763.5 1.473 1.054 2.236 1.502.737.448 1.316 1.054 1.79 1.74.58.816 1.237 1.554 1.5 2.555l.394 1.74c.08.316.264.632.185 1-.133.66-.238 1.345-.343 2.004-.052.265-.105.53-.078.79.05.82-.265 1.53-.58 2.268-.106.237-.264.475-.395.738a.798.798 0 0 0 .21.106l.237-.474c.027 0 .027 0 .053.027-.132.368-.237.764-.37 1.133-.314.817-.63 1.66-1.025 2.45-.21.448-.58.817-.842 1.24-.262.368-.473.763-.736 1.106-.237.29-.473.58-.79.79-.71.527-1.447 1.054-2.21 1.476-.473.29-1.026.448-1.552.58zm-14.027-1.4l-.026.027c-.055-.026-.134-.052-.186-.105l-.632-.95c-.052-.078-.08-.157-.052-.262.29.448.58.87.895 1.29zm16.37 3.61c1.183-.5 2.157-1.21 3.05-2.028.133-.132.264-.263.422-.37 1.106-.684 1.92-1.633 2.658-2.687.842-1.212 1.395-2.582 2.08-3.873a2.73 2.73 0 0 1 .157-.29c-.053 3.004.29 5.955.684 8.933-2.973.105-6 .21-9.052.316zm26.683-.79c-.026.053-.08.106-.105.16-.027-.054-.027-.133-.053-.24-.158.423-.5.212-.737.212-1.42.027-2.868.027-4.29.027-1.368 0-2.762 0-4.13.024-.448 0-.922.105-1.37.132-1.078.052-2.157.08-3.236.105-.08 0-.158-.13-.29-.236a1.81 1.81 0 0 1-.158.237c-.028-.052-.08-.104-.133-.183-.026.08-.053.158-.08.21H58c-.053-.368-.158-.71-.158-1.08 0-.79.08-1.58.105-2.372.027-.368 0-.71 0-1.054.106.08.185.133.29.21.052-.103.105-.182.158-.26 0 0-.053-.028-.106-.08.05-.027.104-.08.104-.106.026-.08.08-.158.08-.21 0-.185-.054-.343-.08-.5.026 0 .052 0 .08-.028l.157.79h.08c-.106-.183.236-.342-.053-.552-.026-.027.026-.185.026-.264-.08-.157-.13-.315-.21-.526.026-.026.105-.053.184-.08-.105-.052-.184-.104-.263-.13.263-.238.263-.37.026-.633.054-.025.106-.025.106-.05 0-.238 0-.475-.052-.71-.053-.266.08-.58-.316-.74a.79.79 0 0 0 .105.21s-.08.027-.158.08c-.342-.317-.13-.74-.21-1.213.184.053.316.106.447.16-.053-.186-.184-.397-.263-.634h-.107v-1.74c0 .027.184.027.29.054 0-.027.025-.053.025-.08-.08-.105-.185-.21-.29-.342l.053-.053c-.21-.262-.105-.63-.105-.71V39.4c.264.264-.13.606.264.764v-.263h-.027c-.026-.395-.026-.79-.052-1.186h-.052c-.027.054-.027.08-.054.133h-.052l.158-6.298c.263.342.552.66.736 1 .606 1.108 1.395 2.057 2.132 3.058.632.87 1.21 1.818 1.79 2.714.71 1.08 1.394 2.16 2.105 3.24a81.41 81.41 0 0 0 1.63 2.426c.5.71 1.028 1.396 1.554 2.082.446.606.92 1.212 1.367 1.818.527.738 1.053 1.475 1.58 2.187.262.368.552.737.84 1.106.16.21.396.37.554.5-.025 0-.052 0-.104-.026.08.105.13.184.184.237.29.158.316.316.158.554zM74 46.854v-.185c0 .052.026.13 0 .184zm.895-11.62c-.027 0-.184-.16-.21-.186-.027.08 0 .158-.053.264-.027-.078-.21-.052-.21-.13-.027.368.157.737.13 1.106.08-.053.395-.08.474-.158.027.026.08.052.106.052-.527.396-.395.79-.158 1.24.052.104.21.315.052.526-.052.053.027.21.053.343h.077v.05l-.237.08c-.052-.08-.367-.236-.367-.37v1.346c.263.08.263.448.368.633a.768.768 0 0 0 .107-.21l.027.024c-.027.158-.053.316-.106.475-.052.236-.105.447-.13.684 0 .026.05.08.05.105-.288.66-.13 1.396-.235 2.08-.08.5 0 1.03-.053 1.556-.054.448-.16.922-.264 1.37-.027.08-.08.105-.21.158.052-.316.026-.527-.027-.817-.028 0-.37-.184-.397-.184 0 .37.21.87.29 1.29-.08-.026-.395-.21-.42-.21-.054.316-.054.738-.08 1.08-.027.264-.263.5-.29.79 0 .16.184.264.158.528h.21c0-.526.238-1 .238-1.554h.078c.027.053.106.106.08.132-.053.29-.16.606-.132.896 0 .158.13.316.08.5-.054.16-.08.317-.107.554-.027-.132-.053-.184-.053-.263-.026 0-.263-.027-.29-.027-.026.158.185.316.158.448-.026.026-.052.026-.105.053l-.868-1.266c-.686-1-1.37-2.003-2.054-3.03a6.312 6.312 0 0 1-.475-.79 37.09 37.09 0 0 0-2.71-4.033c-.762-.974-1.37-2.03-2.08-3.055-.656-.975-1.314-1.924-1.972-2.9-.237-.315-.526-.605-.737-.948-.683-1.08-1.29-2.187-1.972-3.267-.58-.897-1.21-1.767-1.816-2.636-.21-.29-.42-.607-.632-.923a.37.37 0 0 1-.052-.182c-.053-.58-.106-1.16-.132-1.713 0-.527.053-1.054.053-1.608v-.474c0-.132.025-.237.025-.37.025-.025.052-.078.078-.104-.763 0-1.553-.028-2.316 0-.5.025-.763-.186-1.105-.555-1-1.133-1.737-2.424-2.605-3.636a162.42 162.42 0 0 0-2.5-3.427c-.685-.922-1.37-1.818-2.053-2.74-.764-1.054-1.5-2.108-2.29-3.162a381.983 381.983 0 0 0-2.895-3.794c-.45-.58-.95-1.133-1.45-1.74.343.054.66.106.975.133l1.264.08c.947.077 1.894.13 2.84.26.79.107 1.58.265 2.396.396 1.738.29 3.448.765 5.106 1.318.974.316 1.92.738 2.87 1.133 2.13.87 4.157 1.924 6.157 3.03.63.343 1 .896 1.472 1.397.685.712 1.37 1.423 2.027 2.16.762.87 1.472 1.766 2.21 2.662.657.79 1.34 1.58 2 2.372.21.237.37.527.552.79.42.633.895 1.24 1.263 1.924.262.502.42 1.082.604 1.635.262.817.526 1.607.79 2.424.183.606.34 1.24.472 1.87.106.423.08.87.21 1.29.16.556 0 1.16.16 1.715.025.053.05.132.078.185.105.104.184.21.026.368-.025.026-.025.13 0 .21.054-.052.08-.105.133-.184 0 .053.025.08.025.105 0 .104-.027.21 0 .315 0 .052.052.13.078.184.053-.054.105-.08.21-.16.237.897.264 1.793.264 2.715 0 .87.157 1.74-.21 2.583.078-.29-.106-.555-.027-.818z"/><path d="M58.08 45.482c.025 0 .052.027.052.027l-.027-.03c0-.025 0-.025-.026 0zm4.157 26.036c-.29.21-.58.395-.948.474-.028-.026-.028-.053-.054-.08.29-.184.605-.368.895-.553.027.05.08.104.106.157zM12.895 35.81c.29-.367.58-.736.894-1.105.025.026.235.08.262.105-.29.37-.685.87-.974 1.265-.054-.053-.133-.237-.185-.264zM5.42 48.725c-.21-.448-.42-.923-.63-1.37a.91.91 0 0 1 .236-.106c.29.42.42.92.632 1.37 0 0-.21.105-.237.105zm6.712-12.65c-.158.238-.316.502-.474.74-.026-.028-.316.104-.342.078.158-.237.552-.66.71-.896.027.026.053.053.106.08zM59.422 72.6c.025 0 .025-.026.052-.026.184.026.394.052.605.052-.344.237-.555.21-.66-.026zm-47.24-35.418c.028-.08.08-.158.133-.237.052 0 .13-.027.13-.027.107-.184.107-.316.212-.474-.026-.026-.053-.026-.08-.053-.157.108-.315.24-.473.345.053.052.053.08.053.132-.21-.027-.29.08-.395.368-.026.08-.158.106-.29.21-.026.054-.052.186-.105.317l.027.028c-.053.053-.132.08-.132.08-.158.157-.342.29-.5.447-.026.08-.052.158-.052.237.185-.184.5-.527.737-.738l.027.027c.105-.158.184-.316.29-.474.025.026.025.052.052.08-.08.21-.158.446-.237.657-.055.026-.134.08-.134.053-.105.08-.184.184-.29.263l-.473.316c-.263.237-.526.447-.816.685-.184.29-.368.553-.58.896.317-.08.396.053.37.317.368.052.395-.237.5-.448.026-.054.053-.16.105-.186.237-.21.5-.394.763-.605.053-.053.053-.16.053-.238 0-.026-.133-.026-.212-.053.237-.264.58-.71.816-1 .132-.08.263-.186.263-.265-.026-.29.158-.368.37-.474-.106-.08-.133-.157-.133-.183z"/><path d="M12.71 36.892c-.105.184-.21.342-.315.527l-.158-.08c-.105.605-.474 1.132-.842 1.237.105.053.21.106.29.08.078-.027.13-.16.183-.238l.71-1.028.238-.396-.105-.105zM3.948 48.46c.132 0 .264.026.42.026 0-.105.133-.08.133-.184h.08c0 .132.026.237.026.37h-.552c-.027-.027-.132-.186-.106-.212zm-.21-1.212c-.08-.08-.21-.158-.21-.237-.027-.104.052-.235.13-.367.054.184.08.342.132.527-.027.025-.053.052-.053.078zm.658-1.687c.105.266.21.556.316.82a.798.798 0 0 0-.21.105c-.105-.264-.237-.554-.342-.817a.652.652 0 0 1 .237-.106zm58.58 25.194c.13-.052.288-.08.5-.13-.238.183-.422.315-.58.473-.027-.026-.053-.053-.08-.053.053-.105.106-.184.16-.29zM30.63 15.074c.157-.106.29-.185.447-.29l.052.052c-.16.21-.29.42-.475.685-.026-.183-.026-.29-.053-.42-.026 0 0 0 .027-.026zm7.71 13.333c.237-.106.474-.21.763-.343-.026.158-.026.264-.026.37a.927.927 0 0 0-.264-.054c-.158.027-.448.238-.58.264-.025 0 .106-.21.106-.237zm19.74 22.346c.052.263.552.395.052.658.08.055.157.08.236.134a.2.2 0 0 1-.052.106c-.053.025-.158.078-.21.05-.027 0-.08-.104-.08-.157 0-.237.027-.474.053-.79z"/></g></symbol><symbol viewBox="0 0 24 24" id="powerpoint" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m7 1.5V9h5.5L13 3.5M8 11v2h1v6H8v1h4v-1h-1v-2h2a3 3 0 0 0 3-3 3 3 0 0 0-3-3H8m5 2a1 1 0 0 1 1 1 1 1 0 0 1-1 1h-2v-2h2z" fill="#d14524"/></symbol><symbol viewBox="0 0 67.47 70" id="powershell" xmlns="http://www.w3.org/2000/svg"><path d="M18.545 12.4c-3.014 0-6.08 2.34-6.873 5.248L1.91 53.438c-.793 2.908.996 5.248 4.01 5.248h42.887c3.014 0 6.08-2.34 6.873-5.248l9.761-35.79c.794-2.908-.993-5.248-4.007-5.248h-42.89zm4.848 6.243c.652.04 1.29.33 1.76.86l7.96 9.013-3.957 3.246 3.957-3.244 4.832 5.47c.037.042.06.088.094.131.026.034.057.06.082.096.02.028.032.057.05.086.057.087.105.176.15.267.028.06.055.117.08.178a2.546 2.546 0 0 1 .171.764c.005.073.01.146.008.219-.002.09-.01.178-.021.267a2.53 2.53 0 0 1-.036.217 2.56 2.56 0 0 1-.07.252c-.024.076-.048.15-.08.224a2.547 2.547 0 0 1-.111.22 2.503 2.503 0 0 1-.133.218 2.546 2.546 0 0 1-.147.187c-.058.07-.118.137-.185.202-.027.026-.048.057-.076.082-.037.032-.077.054-.116.084-.038.03-.07.065-.11.093L16.8 52.271a2.552 2.552 0 0 1-3.563-.626 2.553 2.553 0 0 1 .63-3.563l18.349-12.853-3.06-3.467-7.839-8.873a2.549 2.549 0 0 1 .225-3.608 2.546 2.546 0 0 1 1.85-.638zm22.441 28.214c1.377 0 2.255 1.083 1.969 2.43-.287 1.347-1.627 2.433-3.004 2.434l-9.957.006c-1.378 0-2.256-1.083-1.969-2.43.287-1.347 1.626-2.433 3.004-2.434l9.957-.006z" fill="#03a9f4" stroke-width="5.342" stroke-linejoin="round"/></symbol><symbol viewBox="0 0 210 210" id="prettier" xmlns="http://www.w3.org/2000/svg"><title>prettier-icon-dark</title><g transform="matrix(.9 0 0 .9 10.5 10.5)" fill="none" fill-rule="evenodd"><rect fill="#56B3B4" x="165" y="40" width="20" height="10" rx="5"/><rect fill="#EA5E5E" x="15" y="200" width="60" height="10" rx="5"/><rect fill="#BF85BF" x="135" y="120" width="40" height="10" rx="5"/><rect fill="#EA5E5E" x="75" y="120" width="50" height="10" rx="5"/><rect fill="#56B3B4" x="15" y="120" width="50" height="10" rx="5"/><rect fill="#BF85BF" x="15" y="160" width="60" height="10" rx="5"/><rect fill="#BF85BF" x="15" y="80" width="60" height="10" rx="5"/><rect fill="#F7BA3E" x="65" y="20" width="110" height="10" rx="5"/><rect fill="#EA5E5E" x="15" y="20" width="40" height="10" rx="5"/><rect fill="#F7BA3E" x="55" y="180" width="20" height="10" rx="5"/><rect fill="#56B3B4" x="55" y="60" width="20" height="10" rx="5"/><rect fill="#56B3B4" x="15" y="180" width="30" height="10" rx="5"/><rect fill="#F7BA3E" x="15" y="60" width="30" height="10" rx="5"/><rect fill="#56B3B4" x="95" y="100" width="90" height="10" rx="5"/><rect fill="#F7BA3E" x="45" y="100" width="40" height="10" rx="5"/><rect fill="#EA5E5E" x="15" y="100" width="20" height="10" rx="5"/><rect fill="#BF85BF" x="105" y="40" width="50" height="10" rx="5"/><rect fill="#56B3B4" x="15" y="40" width="80" height="10" rx="5"/><rect fill="#F7BA3E" x="45" y="140" width="100" height="10" rx="5"/><rect fill="#BF85BF" x="15" y="140" width="20" height="10" rx="5"/><rect fill="#EA5E5E" x="135" y="60" width="60" height="10" rx="5"/><rect fill="#F7BA3E" x="135" y="80" width="60" height="10" rx="5"/><rect fill="#56B3B4" x="15" width="130" height="10" rx="5"/></g></symbol><symbol viewBox="0 0 80 80" id="protractor" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="hxa"><path transform="scale(1 -1)" fill="#564b55" stroke-width="27.224" d="M-2.983-69.251h69.412v67.108H-2.983z"/></clipPath></defs><g transform="matrix(1.13039 0 0 -1.13039 5.714 82.137)" clip-path="url(#hxa)"><g transform="scale(.1)"><path d="M1180.54 92.324c-5.53 0-9.93-1.797-13.23-5.39-3.29-3.614-5.22-8.594-5.81-14.97h36.02c0 6.583-1.47 11.622-4.4 15.126-2.93 3.496-7.12 5.234-12.58 5.234zm2.84-62.656c-10.19 0-18.22 3.086-24.11 9.297-5.88 6.21-8.83 14.824-8.83 25.84 0 11.101 2.73 19.922 8.21 26.464 5.45 6.524 12.81 9.805 22.02 9.805 8.63 0 15.46-2.851 20.48-8.523 5.03-5.676 7.55-13.157 7.55-22.461v-6.613h-47.45c.21-8.086 2.26-14.22 6.12-18.418 3.89-4.18 9.34-6.29 16.38-6.29 7.42 0 14.76 1.563 22 4.669V34.14c-3.68-1.602-7.18-2.746-10.48-3.438-3.28-.684-7.24-1.035-11.89-1.035M1272.34 30.918v44.57c0 5.606-1.28 9.805-3.82 12.559-2.56 2.773-6.56 4.16-12.02 4.16-7.2 0-12.49-1.953-15.84-5.851-3.34-3.895-5.03-10.32-5.03-19.286V30.918h-10.42v68.887h8.47l1.71-9.422h.5c2.14 3.387 5.14 6.023 8.99 7.887 3.85 1.867 8.15 2.804 12.88 2.804 8.29 0 14.54-2.011 18.73-6.015 4.19-3.985 6.28-10.391 6.28-19.192V30.918h-10.43M1328.96 38.406c7.1 0 12.27 1.938 15.48 5.813 3.22 3.879 4.81 10.129 4.81 18.758v2.199c0 9.765-1.62 16.726-4.87 20.898-3.25 4.18-8.44 6.25-15.56 6.25-6.11 0-10.79-2.383-14.04-7.129-3.26-4.746-4.88-11.472-4.88-20.136 0-8.797 1.61-15.45 4.84-19.93 3.23-4.484 7.97-6.723 14.22-6.723zm20.85 1.762h-.56c-4.83-7.004-12.02-10.5-21.62-10.5-9.01 0-16.03 3.066-21.04 9.238-5 6.153-7.5 14.922-7.5 26.27 0 11.355 2.51 20.176 7.54 26.465 5.03 6.289 12.03 9.433 21 9.433 9.34 0 16.5-3.398 21.49-10.195h.81l-.43 4.96-.25 4.845v28.039h10.43V30.918h-8.49l-1.38 9.25M1434.91 38.27c1.85 0 3.63.136 5.34.421 1.72.274 3.09.547 4.1.84v-7.976c-1.15-.559-2.81-.996-5.01-1.36-2.18-.351-4.17-.527-5.94-.527-13.32 0-19.97 7.012-19.97 21.055V91.71h-9.88v5.027l9.88 4.336 4.38 14.707h6.04V99.805h20V91.71h-20V51.16c0-4.15.98-7.333 2.96-9.56 1.97-2.206 4.67-3.331 8.1-3.331M1463.81 65.43c0-8.809 1.76-15.508 5.27-20.118 3.53-4.609 8.69-6.906 15.53-6.906s12.01 2.297 15.56 6.875c3.53 4.602 5.3 11.301 5.3 20.149 0 8.75-1.77 15.41-5.3 19.953-3.55 4.539-8.77 6.824-15.69 6.824-6.82 0-11.99-2.246-15.47-6.73-3.46-4.48-5.2-11.16-5.2-20.047zm52.47 0c0-11.23-2.83-20-8.48-26.309-5.66-6.309-13.47-9.453-23.44-9.453-6.17 0-11.64 1.445-16.42 4.336-4.78 2.89-8.46 7.031-11.06 12.45-2.59 5.401-3.88 11.73-3.88 18.976 0 11.23 2.8 19.968 8.41 26.242 5.61 6.258 13.4 9.402 23.38 9.402 9.64 0 17.3-3.222 22.97-9.62 5.69-6.415 8.52-15.087 8.52-26.024M1591.71 92.324c-5.54 0-9.94-1.797-13.23-5.39-3.3-3.614-5.24-8.594-5.81-14.97h36c0 6.583-1.46 11.622-4.39 15.126-2.93 3.496-7.13 5.234-12.57 5.234zm2.83-62.656c-10.19 0-18.22 3.086-24.11 9.297-5.89 6.21-8.83 14.824-8.83 25.84 0 11.101 2.74 19.922 8.2 26.464 5.46 6.524 12.81 9.805 22.04 9.805 8.62 0 15.45-2.851 20.48-8.523 5.03-5.676 7.54-13.157 7.54-22.461v-6.613h-47.45c.21-8.086 2.25-14.22 6.13-18.418 3.87-4.18 9.33-6.29 16.36-6.29 7.43 0 14.77 1.563 22.01 4.669V34.14c-3.69-1.602-7.17-2.746-10.46-3.438-3.3-.684-7.27-1.035-11.91-1.035M1683.5 30.918v44.57c0 5.606-1.27 9.805-3.83 12.559-2.55 2.773-6.55 4.16-12.01 4.16-7.2 0-12.48-1.953-15.83-5.851-3.35-3.895-5.03-10.32-5.03-19.286V30.918h-10.43v68.887h8.48l1.69-9.422h.51c2.14 3.387 5.14 6.023 8.99 7.887 3.84 1.867 8.15 2.804 12.88 2.804 8.3 0 14.54-2.011 18.74-6.015 4.19-3.985 6.29-10.391 6.29-19.192V30.918h-10.45M1740.11 38.406c7.12 0 12.28 1.938 15.49 5.813 3.21 3.879 4.81 10.129 4.81 18.758v2.199c0 9.765-1.62 16.726-4.87 20.898-3.25 4.18-8.43 6.25-15.56 6.25-6.12 0-10.8-2.383-14.05-7.129-3.24-4.746-4.88-11.472-4.88-20.136 0-8.797 1.64-15.45 4.85-19.93 3.22-4.484 7.96-6.723 14.21-6.723zm20.87 1.762h-.57c-4.82-7.004-12.03-10.5-21.62-10.5-9.01 0-16.02 3.066-21.03 9.238-5 6.153-7.52 14.922-7.52 26.27 0 11.355 2.52 20.176 7.55 26.465 5.02 6.289 12.02 9.433 21 9.433 9.34 0 16.5-3.398 21.48-10.195h.83l-.44 4.96-.25 4.845v28.039h10.43V30.918h-8.49l-1.37 9.25M1846.07 38.27c1.85 0 3.64.136 5.36.421 1.7.274 3.07.547 4.08.84v-7.976c-1.13-.559-2.8-.996-5-1.36-2.2-.351-4.18-.527-5.94-.527-13.33 0-19.99 7.012-19.99 21.055V91.71h-9.86v5.027l9.86 4.336 4.4 14.707h6.04V99.805H1855V91.71h-19.98V51.16c0-4.15.98-7.333 2.95-9.56 1.97-2.206 4.68-3.331 8.1-3.331M1894.26 92.324c-5.53 0-9.94-1.797-13.22-5.39-3.31-3.614-5.25-8.594-5.83-14.97h36.01c0 6.583-1.45 11.622-4.38 15.126-2.95 3.496-7.13 5.234-12.58 5.234zm2.83-62.656c-10.19 0-18.22 3.086-24.1 9.297-5.9 6.21-8.84 14.824-8.84 25.84 0 11.101 2.73 19.922 8.2 26.464 5.47 6.524 12.81 9.805 22.03 9.805 8.63 0 15.46-2.851 20.49-8.523 5.03-5.676 7.55-13.157 7.55-22.461v-6.613h-47.46c.22-8.086 2.26-14.22 6.13-18.418 3.87-4.18 9.33-6.29 16.37-6.29 7.42 0 14.75 1.563 22 4.669V34.14c-3.7-1.602-7.17-2.746-10.47-3.438-3.28-.684-7.25-1.035-11.9-1.035M1983.36 49.727c0-6.426-2.4-11.368-7.18-14.844-4.77-3.477-11.47-5.215-20.11-5.215-9.13 0-16.26 1.445-21.37 4.336v9.687a51.32 51.32 0 0 1 10.65-3.964c3.79-.977 7.45-1.457 10.97-1.457 5.46 0 9.64.87 12.57 2.609 2.95 1.738 4.41 4.394 4.41 7.95 0 2.694-1.17 4.98-3.5 6.894-2.32 1.914-6.85 4.152-13.6 6.757-6.41 2.383-10.97 4.473-13.67 6.25-2.71 1.778-4.72 3.81-6.04 6.067-1.31 2.254-1.98 4.96-1.98 8.113 0 5.606 2.29 10.04 6.86 13.281 4.57 3.25 10.84 4.883 18.79 4.883 7.42 0 14.66-1.515 21.74-4.531l-3.71-8.496c-6.9 2.851-13.17 4.277-18.79 4.277-4.94 0-8.67-.77-11.18-2.324-2.52-1.543-3.78-3.691-3.78-6.406 0-1.844.48-3.418 1.42-4.707.95-1.309 2.46-2.54 4.56-3.711 2.09-1.184 6.11-2.871 12.07-5.086 8.16-2.98 13.69-5.98 16.55-8.996 2.87-3.02 4.32-6.809 4.32-11.367M2021.28 38.27c1.85 0 3.64.136 5.35.421 1.71.274 3.09.547 4.09.84v-7.976c-1.14-.559-2.81-.996-5.01-1.36-2.18-.351-4.18-.527-5.93-.527-13.33 0-19.99 7.012-19.99 21.055V91.71h-9.87v5.027l9.87 4.336 4.4 14.707h6.02V99.805h20V91.71h-20V51.16c0-4.15 1-7.333 2.97-9.56 1.98-2.206 4.67-3.331 8.1-3.331M2053.61 30.918h-10.42v68.887h10.42zm-11.31 87.559c0 2.39.59 4.14 1.76 5.253 1.18 1.106 2.65 1.661 4.42 1.661 1.67 0 3.1-.567 4.32-1.7 1.22-1.132 1.82-2.871 1.82-5.214 0-2.344-.6-4.09-1.82-5.247-1.22-1.16-2.65-1.726-4.32-1.726-1.77 0-3.24.566-4.42 1.726-1.17 1.157-1.76 2.903-1.76 5.247M2121.59 30.918v44.57c0 5.606-1.27 9.805-3.83 12.559-2.55 2.773-6.55 4.16-12 4.16-7.21 0-12.49-1.953-15.84-5.851-3.35-3.895-5.03-10.32-5.03-19.286V30.918h-10.43v68.887h8.49l1.69-9.422h.5c2.15 3.387 5.14 6.023 8.99 7.887 3.85 1.867 8.16 2.804 12.88 2.804 8.3 0 14.54-2.011 18.74-6.015 4.19-3.985 6.29-10.391 6.29-19.192V30.918h-10.45M2159.29 77.742c0-4.812 1.35-8.465 4.08-10.926 2.72-2.48 6.51-3.71 11.37-3.71 10.19 0 15.28 4.953 15.28 14.831 0 10.344-5.16 15.532-15.47 15.532-4.9 0-8.67-1.32-11.31-3.965-2.63-2.649-3.95-6.555-3.95-11.762zm-5.67-58.387c0-3.73 1.58-6.55 4.72-8.488 3.14-1.922 7.65-2.879 13.52-2.879 8.75 0 15.24 1.309 19.45 3.926 4.21 2.617 6.31 6.172 6.31 10.652 0 3.723-1.15 6.32-3.45 7.754-2.31 1.457-6.65 2.168-13.01 2.168h-12.51c-4.74 0-8.43-1.12-11.06-3.386-2.65-2.266-3.97-5.508-3.97-9.747zm54.94 80.45v-6.582l-12.76-1.512c1.18-1.477 2.23-3.39 3.15-5.754.91-2.371 1.37-5.039 1.37-8.02 0-6.746-2.29-12.128-6.91-16.152-4.61-4.012-10.93-6.023-18.98-6.023-2.05 0-3.98.156-5.78.5-4.45-2.356-6.67-5.305-6.67-8.871 0-1.883.77-3.282 2.34-4.176 1.54-.902 4.21-1.36 7.97-1.36h12.2c7.46 0 13.19-1.574 17.19-4.707 4-3.144 6-7.714 6-13.71 0-7.618-3.06-13.426-9.17-17.43C2192.38 2.004 2183.46 0 2171.72 0c-9 0-15.95 1.68-20.82 5.027-4.88 3.352-7.34 8.079-7.34 14.211 0 4.18 1.35 7.813 4.03 10.88 2.68 3.046 6.45 5.116 11.32 6.21-1.77.8-3.24 2.031-4.44 3.711-1.19 1.68-1.78 3.633-1.78 5.84 0 2.52.66 4.707 2.01 6.602 1.34 1.882 3.44 3.71 6.34 5.468-3.56 1.465-6.46 3.953-8.71 7.48-2.23 3.516-3.35 7.54-3.35 12.06 0 7.55 2.26 13.37 6.79 17.452 4.52 4.082 10.93 6.133 19.22 6.133 3.6 0 6.86-.429 9.75-1.27h23.82M2284.61 91.71h-17.54V30.919h-10.43v60.793h-12.31v4.707l12.31 3.766v3.839c0 16.922 7.4 25.391 22.19 25.391 3.65 0 7.93-.73 12.82-2.195l-2.7-8.364c-4.03 1.301-7.46 1.946-10.31 1.946-3.93 0-6.85-1.309-8.73-3.926-1.89-2.617-2.84-6.816-2.84-12.598v-4.472h17.54V91.71M2302.87 65.43c0-8.809 1.76-15.508 5.28-20.118 3.52-4.609 8.7-6.906 15.52-6.906 6.84 0 12.02 2.297 15.57 6.875 3.54 4.602 5.3 11.301 5.3 20.149 0 8.75-1.76 15.41-5.3 19.953-3.55 4.539-8.78 6.824-15.69 6.824-6.83 0-11.99-2.246-15.46-6.73-3.48-4.48-5.22-11.16-5.22-20.047zm52.48 0c0-11.23-2.82-20-8.47-26.309-5.67-6.309-13.48-9.453-23.46-9.453-6.15 0-11.62 1.445-16.4 4.336-4.77 2.89-8.47 7.031-11.06 12.45-2.59 5.401-3.9 11.73-3.9 18.976 0 11.23 2.81 19.968 8.43 26.242 5.6 6.258 13.4 9.402 23.38 9.402 9.63 0 17.28-3.222 22.97-9.62 5.68-6.415 8.51-15.087 8.51-26.024M2403.79 101.074c3.07 0 5.8-.254 8.22-.761l-1.43-9.676c-2.86.633-5.37.933-7.55.933-5.58 0-10.33-2.261-14.3-6.785-3.95-4.531-5.94-10.156-5.94-16.902V30.918h-10.43v68.887h8.62l1.19-12.754h.5c2.56 4.48 5.63 7.949 9.23 10.37 3.61 2.423 7.56 3.653 11.89 3.653M2500.33 69.766l-10.68 28.476c-1.39 3.594-2.81 8.028-4.28 13.262-.93-4.024-2.24-8.438-3.96-13.262l-10.81-28.476zm14.77-38.848l-11.44 29.227h-36.83l-11.32-29.227h-10.81l36.34 92.273h8.98l36.13-92.273h-11.05M2583.07 30.918v44.57c0 5.606-1.27 9.805-3.83 12.559-2.55 2.773-6.55 4.16-12 4.16-7.21 0-12.49-1.953-15.84-5.851-3.35-3.895-5.03-10.32-5.03-19.286V30.918h-10.43v68.887h8.48l1.69-9.422h.51c2.14 3.387 5.14 6.023 8.99 7.887 3.84 1.867 8.15 2.804 12.88 2.804 8.3 0 14.54-2.011 18.74-6.015 4.19-3.985 6.29-10.391 6.29-19.192V30.918h-10.45M2620.76 77.742c0-4.812 1.36-8.465 4.08-10.926 2.73-2.48 6.53-3.71 11.37-3.71 10.2 0 15.28 4.953 15.28 14.831 0 10.344-5.15 15.532-15.45 15.532-4.91 0-8.68-1.32-11.32-3.965-2.64-2.649-3.96-6.555-3.96-11.762zm-5.66-58.387c0-3.73 1.57-6.55 4.71-8.488 3.15-1.922 7.65-2.879 13.53-2.879 8.75 0 15.23 1.309 19.44 3.926 4.21 2.617 6.31 6.172 6.31 10.652 0 3.723-1.14 6.32-3.45 7.754-2.31 1.457-6.64 2.168-13 2.168h-12.51c-4.74 0-8.43-1.12-11.07-3.386-2.63-2.266-3.96-5.508-3.96-9.747zm54.94 80.45v-6.582l-12.76-1.512c1.18-1.477 2.22-3.39 3.14-5.754.92-2.371 1.38-5.039 1.38-8.02 0-6.746-2.3-12.128-6.92-16.152-4.61-4.012-10.92-6.023-18.97-6.023-2.05 0-3.99.156-5.78.5-4.46-2.356-6.67-5.305-6.67-8.871 0-1.883.78-3.282 2.33-4.176 1.55-.902 4.21-1.36 7.98-1.36h12.2c7.46 0 13.18-1.574 17.18-4.707 4.01-3.144 6-7.714 6-13.71 0-7.618-3.06-13.426-9.17-17.43C2653.87 2.004 2644.94 0 2633.2 0c-9 0-15.95 1.68-20.83 5.027-4.88 3.352-7.33 8.079-7.33 14.211 0 4.18 1.35 7.813 4.02 10.88 2.69 3.046 6.47 5.116 11.32 6.21-1.77.8-3.23 2.031-4.43 3.711-1.19 1.68-1.79 3.633-1.79 5.84 0 2.52.66 4.707 2.01 6.602 1.35 1.882 3.45 3.71 6.35 5.468-3.56 1.465-6.47 3.953-8.71 7.48-2.23 3.516-3.35 7.54-3.35 12.06 0 7.55 2.25 13.37 6.79 17.452 4.52 4.082 10.92 6.133 19.21 6.133 3.62 0 6.86-.429 9.75-1.27h23.83M2692.7 99.805V55.117c0-5.605 1.27-9.805 3.83-12.566 2.56-2.766 6.57-4.145 12.01-4.145 7.2 0 12.47 1.965 15.81 5.903 3.33 3.945 4.99 10.379 4.99 19.304v36.192h10.44V30.918h-8.62l-1.5 9.25h-.58c-2.13-3.41-5.1-5.988-8.88-7.793-3.8-1.809-8.13-2.707-12.99-2.707-8.37 0-14.65 1.992-18.81 5.977-4.18 3.964-6.26 10.351-6.26 19.101v45.059h10.56M2760.61 30.918h10.43v97.805h-10.43zM2810.67 38.27c6.5 0 11.6 1.789 15.31 5.343 3.71 3.575 5.56 8.555 5.56 14.961v6.23l-10.44-.448c-8.3-.286-14.27-1.583-17.94-3.868-3.66-2.273-5.5-5.82-5.5-10.644 0-3.781 1.14-6.64 3.42-8.613 2.29-1.973 5.48-2.961 9.59-2.961zm23.57-7.352l-2.07 9.805h-.51c-3.44-4.305-6.86-7.227-10.27-8.77-3.42-1.523-7.68-2.285-12.8-2.285-6.83 0-12.17 1.758-16.05 5.273-3.87 3.528-5.81 8.536-5.81 15.032 0 13.906 11.12 21.199 33.37 21.875l11.7.359v4.277c0 5.418-1.17 9.395-3.5 11.985-2.32 2.566-6.03 3.855-11.15 3.855-5.74 0-12.24-1.758-19.49-5.273l-3.21 7.988c3.4 1.836 7.11 3.281 11.16 4.324a47.81 47.81 0 0 0 12.16 1.575c8.23 0 14.3-1.817 18.27-5.461 3.96-3.66 5.93-9.5 5.93-17.54V30.919h-7.73M2893.6 101.074c3.07 0 5.8-.254 8.25-.761l-1.46-9.676c-2.84.633-5.35.933-7.54.933-5.56 0-10.33-2.261-14.3-6.785-3.96-4.531-5.93-10.156-5.93-16.902V30.918h-10.44v68.887h8.61l1.19-12.754h.5c2.57 4.48 5.65 7.949 9.25 10.37 3.6 2.423 7.56 3.653 11.87 3.653M2901.63 6.727c-3.94 0-7.04.558-9.31 1.691v9.121c2.97-.84 6.08-1.25 9.31-1.25 4.14 0 7.3 1.25 9.45 3.77 2.16 2.507 3.24 6.132 3.24 10.859v91.895h10.69V31.797c0-7.95-2.01-14.121-6.04-18.496-4.02-4.383-9.8-6.574-17.34-6.574M2999.96 55.371c0-8.086-2.93-14.394-8.8-18.918-5.87-4.52-13.83-6.785-23.88-6.785-10.9 0-19.27 1.406-25.14 4.219v10.3c3.77-1.59 7.88-2.847 12.31-3.765 4.45-.93 8.85-1.399 13.21-1.399 7.12 0 12.49 1.36 16.09 4.063 3.59 2.695 5.4 6.465 5.4 11.277 0 3.196-.63 5.805-1.91 7.832-1.29 2.024-3.42 3.907-6.42 5.625-2.99 1.711-7.56 3.664-13.67 5.84-8.55 3.059-14.66 6.692-18.32 10.871-3.66 4.2-5.51 9.668-5.51 16.407 0 7.089 2.68 12.714 7.99 16.914 5.32 4.191 12.36 6.289 21.12 6.289 9.13 0 17.54-1.68 25.2-5.032l-3.32-9.304c-7.59 3.183-14.96 4.785-22.13 4.785-5.66 0-10.07-1.223-13.26-3.652-3.19-2.43-4.78-5.809-4.78-10.118 0-3.191.59-5.8 1.76-7.832 1.17-2.031 3.14-3.886 5.95-5.597 2.78-1.688 7.04-3.563 12.79-5.625 9.63-3.426 16.26-7.118 19.89-11.063 3.62-3.937 5.43-9.043 5.43-15.332M741.648 375.406h30c28.965 0 50.227 5.039 63.774 15.117 13.531 10.079 20.32 25.821 20.32 47.247 0 19.832-6.074 34.628-18.191 44.402-12.141 9.758-31.028 14.641-56.692 14.641h-39.211zm172.192 64.246c0-36.062-11.809-63.691-35.434-82.898-23.621-19.219-57.234-28.82-100.847-28.82h-35.911V198.73h-56.445v345.329h99.438c43.14 0 75.457-8.829 96.961-26.465 21.496-17.637 32.238-43.614 32.238-77.942M1099.26 464.691c11.17 0 20.39-.789 27.63-2.371l-5.43-51.718c-7.88 1.894-16.07 2.832-24.57 2.832-22.2 0-40.19-7.246-53.97-21.731-13.78-14.48-20.66-33.301-20.66-56.453V198.73h-55.514v261.227h43.464l7.32-46.055h2.83c8.66 15.594 19.96 27.95 33.9 37.09 13.93 9.141 28.93 13.699 45 13.699M1206.88 329.82c0-60.308 22.28-90.465 66.85-90.465 44.08 0 66.13 30.157 66.13 90.465 0 59.688-22.21 89.512-66.61 89.512-23.31 0-40.2-7.707-50.67-23.144-10.47-15.43-15.7-37.54-15.7-66.368zm190.13 0c0-42.672-10.95-75.972-32.83-99.898-21.89-23.945-52.35-35.918-91.41-35.918-24.41 0-45.97 5.508-64.7 16.543-18.75 11.016-33.16 26.836-43.23 47.48-10.08 20.625-15.11 44.551-15.11 71.793 0 42.364 10.86 75.43 32.58 99.2 21.73 23.777 52.36 35.671 91.89 35.671 37.79 0 67.7-12.156 89.75-36.492 22.05-24.328 33.06-57.121 33.06-98.379M1558.11 238.887c13.54 0 27.07 2.129 40.62 6.386v-41.816c-6.13-2.676-14.05-4.922-23.73-6.738-9.69-1.797-19.73-2.715-30.12-2.715-52.59 0-78.88 27.715-78.88 83.144v140.778h-35.68v24.558l38.26 20.325 18.9 55.261h34.26v-58.113h74.39v-42.031h-74.39v-139.84c0-13.379 3.34-23.242 10.03-29.629 6.69-6.387 15.48-9.57 26.34-9.57M1783.44 464.691c11.17 0 20.38-.789 27.62-2.371l-5.43-51.718c-7.88 1.894-16.06 2.832-24.56 2.832-22.2 0-40.2-7.246-53.97-21.731-13.78-14.48-20.66-33.301-20.66-56.453V198.73h-55.52v261.227h43.46l7.34-46.055h2.82c8.66 15.594 19.95 27.95 33.9 37.09 13.92 9.141 28.93 13.699 45 13.699M1925.05 236.523c20.15 0 36.32 5.625 48.52 16.895 12.21 11.25 18.31 27.051 18.31 47.344v22.676l-33.54-1.407c-26.13-.937-45.16-5.312-57.04-13.105-11.89-7.793-17.82-19.727-17.82-35.781 0-11.661 3.45-20.665 10.39-27.051 6.91-6.387 17.32-9.571 31.18-9.571zm82.66-37.793l-11.11 36.387h-1.87c-12.62-15.918-25.29-26.738-38.04-32.48-12.74-5.742-29.13-8.633-49.13-8.633-25.67 0-45.7 6.934-60.1 20.801-14.41 13.847-21.62 33.457-21.62 58.808 0 26.934 10 47.246 30 60.934 19.99 13.691 50.45 21.172 91.41 22.441l45.09 1.414v13.938c0 16.699-3.88 29.16-11.68 37.441-7.79 8.262-19.88 12.383-36.25 12.383-13.39 0-26.23-1.953-38.5-5.891a294.638 294.638 0 0 1-35.44-13.933l-17.94 39.668c14.17 7.41 29.68 13.035 46.52 16.894 16.85 3.868 32.77 5.789 47.72 5.789 33.22 0 58.31-7.246 75.22-21.726 16.94-14.492 25.4-37.246 25.4-68.262V198.73h-39.68M2220.04 194.004c-39.52 0-69.55 11.543-90.1 34.609-20.55 23.067-30.82 56.172-30.82 99.321 0 43.925 10.74 77.707 32.23 101.339 21.5 23.614 52.56 35.418 93.18 35.418 27.56 0 52.35-5.117 74.41-15.359l-16.78-44.641c-23.46 9.133-42.82 13.704-58.1 13.704-45.19 0-67.79-29.993-67.79-89.981 0-29.293 5.63-51.305 16.89-66.031 11.26-14.707 27.76-22.09 49.48-22.09 24.72 0 48.11 6.152 70.15 18.437v-48.417c-9.92-5.84-20.5-10-31.76-12.52-11.26-2.52-24.93-3.789-40.99-3.789M2451.52 238.887c13.54 0 27.08 2.129 40.63 6.386v-41.816c-6.15-2.676-14.05-4.922-23.73-6.738-9.69-1.797-19.73-2.715-30.12-2.715-52.6 0-78.9 27.715-78.9 83.144v140.778h-35.66v24.558l38.26 20.325 18.9 55.261h34.26v-58.113h74.39v-42.031h-74.39v-139.84c0-13.379 3.34-23.242 10.03-29.629 6.69-6.387 15.47-9.57 26.33-9.57M2585.92 329.82c0-60.308 22.28-90.465 66.84-90.465 44.09 0 66.15 30.157 66.15 90.465 0 59.688-22.22 89.512-66.62 89.512-23.31 0-40.2-7.707-50.67-23.144-10.47-15.43-15.7-37.54-15.7-66.368zm190.13 0c0-42.672-10.94-75.972-32.83-99.898-21.89-23.945-52.36-35.918-91.4-35.918-24.42 0-45.98 5.508-64.72 16.543-18.74 11.016-33.14 26.836-43.22 47.48-10.07 20.625-15.12 44.551-15.12 71.793 0 42.364 10.87 75.43 32.59 99.2 21.74 23.777 52.36 35.671 91.89 35.671 37.79 0 67.7-12.156 89.75-36.492 22.04-24.328 33.06-57.121 33.06-98.379M2972.33 464.691c11.18 0 20.38-.789 27.63-2.371l-5.43-51.718c-7.87 1.894-16.05 2.832-24.57 2.832-22.2 0-40.19-7.246-53.96-21.731-13.78-14.48-20.67-33.301-20.67-56.453V198.73h-55.51v261.227h43.46l7.33-46.055h2.83c8.66 15.594 19.96 27.95 33.89 37.09 13.94 9.141 28.94 13.699 45 13.699" fill="#100f0d"/><path d="M610.11 372.83c0-170.584-138.257-308.862-308.846-308.862-170.602 0-308.846 138.278-308.846 308.863 0 170.576 138.244 308.846 308.846 308.846 170.59 0 308.846-138.27 308.846-308.846" fill="#e53935" stroke-width="1.029"/><path d="M460.694 521.792l-105.04.958-61.415 61.415-72.096-47.883 12.445-12.438-29.207.26-99.129-166.817H67.357l24.39-24.402-24.57-41.363L294.66 64.049c2.192-.04 4.399-.08 6.603-.08 170.416 0 308.585 138.055 308.846 308.408L460.694 521.792" fill="#d51c2f" stroke-width="1.029"/><path d="M149.093 350.258c0 84.048 68.13 152.151 152.171 152.151 84.028 0 152.139-68.103 152.139-152.151zm342.063-7.017v14.046h44.015c-1.75 59.337-25.556 113.104-63.54 153.419L438.75 477.81l-9.925 9.94 32.875 32.887c-40.314 37.983-94.081 61.79-153.41 63.527l-.015-44.003h-14.035v44.003c-59.34-1.737-113.096-25.556-153.41-63.527l32.887-32.887-9.945-9.92-32.883 32.875c-37.975-40.315-61.781-94.082-63.53-153.419h44.002l-.008-14.034H67.176v-51.511h468.176v51.5h-44.196" fill="#f5f5f5" stroke-width="1.029"/></g></g></symbol><symbol id="pug" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:#c1272d}.hyst1{fill:#efcca3}.st2{fill:#ed1c24}.hyst3{fill:#ccac8d}.hyst4{fill:#fff}.st5{fill:#ff931e}.st6{fill:#ffb81e}.hyst7{fill:#56332b}.hyst8{fill:#442823}.hyst9{fill:#7f4a41}.hyst10{fill:#331712}.st11{fill:#fc6}.st12{fill:#ccc}.st13{fill:#b3b3b3}.st14{fill:#989898}.st15{fill:#323232}.st16{fill:#1e1e1e}.st17{fill:#4c4c4c}.st18{fill:#e6e6e6}.st19{fill:#606060}</style><path class="hyst1" d="M107.4 50.9c-.2-4.4.4-8.3-1.6-11.6-4.8-8.2-16.8-13-40.8-13v.7h-.5.5v-.7c-24 0-36.6 4.8-41.4 13.1-1.9 3.4-1.7 7.2-2 11.6-.2 3.5-1.8 7.2-1.1 11.2.8 5.2 1.1 10.4 1.9 15.2.6 3.9 6 7.2 6.5 10.9 1.4 10.2 12 14.9 36 14.9v.8h-.6.7v-.8c24 0 34.2-4.7 35.5-14.9.5-3.8 5.5-7 6.1-10.9.8-4.8 1.1-10 1.9-15.2.7-4-.9-7.8-1.1-11.3z"/><path class="hyst3" d="M64.6 54.5c4.3.1 7.3 2.8 10.1 5.3 3.3 2.9 8.9 4.9 11.2 7.4 2.3 2.5 5.3 5 6.4 8.9 1.1 3.9 1.4 8.9 1.4 10.2 0 1.3.7 1 2.7 0 4.7-2.3 9.9-8.5 9.9-8.5-.6 3.9-5.7 7.4-6.2 11.1C98.9 99.1 89 104 64.5 104h-.1.6"/><path class="hyst3" d="M80.4 46.7c.9 3.1 4.1 13.6-2.1 10.1 0 0 2.6 1.5 4.2 7.2 1.7 5.7 5.8 6.4 5.8 6.4s6.7 1.3 11.7-3c4.2-3.6 4.9-10 3.1-14.9-1.8-4.8-5-6.3-9.7-7.3-4.7-1.1-14.1-2-13 1.5z"/><circle cx="92.3" cy="58.1" r="8.8"/><circle class="hyst4" cx="90" cy="54.2" r="2.3"/><path class="hyst1" d="M78.9 57.7s7.9 5.4 12.2 10.7c4.3 5.3 4.2 6.3 4.2 6.3l-3.1 1.4s-4.4-8.3-9.8-11.4c-5.5-3.1-6.1-5.7-6.1-5.7l2.6-1.3z"/><path class="hyst3" d="M64.9 54.5c-4.3.1-7.5 2.8-10.4 5.3-3.3 2.9-9.1 4.9-11.4 7.4-2.3 2.5-5.4 5-6.5 8.9-1.1 3.9-1.5 8.9-1.5 10.2 0 1.3.2 1.4-2.7 0-4.7-2.2-9.9-8.5-9.9-8.5.6 3.9 5.7 7.4 6.2 11.1C30.1 99.1 40 104 64.5 104h.5"/><path class="hyst7" d="M88.1 71.4C83.3 65.5 75.6 60 64.9 60h-.1c-10.7 0-18.4 5.5-23.2 11.4-5 6.1-4.6 8.5-4.6 14.3 0 21 7.4 15 12.3 17.6 5 2.5 10.2 1.7 15.5 1.7h.1c5.4 0 10.5.7 15.5-1.8 4.9-2.5 12.3 3.7 12.3-17.3.1-5.8.4-8.4-4.6-14.5z"/><path class="hyst8" d="M64.4 65.2s-.7 9.7-2.1 11.6l2.6-.6-.5-11z"/><path class="hyst8" d="M65.1 65.2s.7 9.7 2.1 11.6l-2.6-.6.5-11z"/><path class="hyst7" d="M56.7 62.9c-1-2.3 2.6-6 8.3-6.1 5.7 0 9.3 3.7 8.3 6.1-1 2.4-4.6 3.1-8.3 3.2-3.6-.1-7.3-.8-8.3-3.2z"/><path d="M65 65.2c0-.4 3.4-.5 5.2-1.7 0 0-3.7 1.2-4.5.7-.8-.4-1-1.6-1-1.6s-.3 1.2-.9 1.6c-.7.4-4.9-.7-4.9-.7s5.6 1.4 5.6 1.7c0 .3-.1 1.3-.1 2 0 2.5 0 8.7.4 9.2.6.9.4-6.7.4-9.2-.1-.8-.1-1.6-.2-2z"/><path class="hyst9" d="M65.2 78.6c1.7 0 4.7 1.2 7.4 3.1-2.6-2.9-5.7-4.9-7.4-4.9-1.8 0-5.6 2.2-8.3 5.4 2.8-2.2 6.4-3.6 8.3-3.6z"/><path class="hyst8" d="M64.5 96.3c-3.8 0-7.5-1.2-10.9-2.1-.7-.2-1.4.3-2.1.1-6.3-2-11.4-5.4-14.5-9.7v1c0 21 7.4 15.1 12.3 17.6 5 2.5 10.2 1.7 15.5 1.7h.1c5.4 0 10.5.7 15.5-1.8 4.9-2.5 12.3 3.6 12.3-17.4 0-.8 0-1.6.1-2.3-2.9 4.7-8.2 8.4-14.8 10.6-.6.2-2-.3-2.6-.2-3.6 1.2-6.8 2.5-10.9 2.5z"/><path class="hyst8" d="M55 85s-2.5 7.5-.8 10.8l-2.3-1s1.7-7.6 3.1-9.8zM74.8 85s2.5 7.5.8 10.8l2.3-1s-1.8-7.6-3.1-9.8z"/><path class="hyst3" d="M48.6 46.7c-.9 3.1-4.1 13.6 2.1 10.1 0 0-2.6 1.5-4.2 7.2s-5.8 6.4-5.8 6.4-6.7 1.3-11.7-3c-4.2-3.6-4.9-10-3.1-14.9s5-6.3 9.7-7.3c4.7-1.1 14-2 13 1.5z"/><path d="M64.9 76.8c2.7 0 11.1 5.8 11.2 12.9v-.4c0-7.4-6.8-13.3-11.2-13.3-4.4 0-11.2 6-11.2 13.3v.4c.1-7.1 8.5-12.9 11.2-12.9z"/><ellipse transform="rotate(-14.465 66.712 61.468)" class="hyst10" cx="66.7" cy="61.5" rx=".8" ry="1.5"/><ellipse transform="rotate(17.235 62.371 61.462)" class="hyst10" cx="62.4" cy="61.5" rx=".8" ry="1.5"/><circle cx="37.2" cy="58.1" r="8.8"/><circle class="hyst4" cx="39.5" cy="54.2" r="2.3"/><path class="hyst9" d="M67.5 58.2c0-.1-2.3 1-2.9 1.1-.6-.1-2.9-1.2-2.9-1.1h5.8z"/><path class="hyst1" d="M50 57.7s-7.9 5.4-12.2 10.7c-4.3 5.3-4.2 6.3-4.2 6.3l3.1 1.4s4.4-8.3 9.8-11.4 6.1-5.7 6.1-5.7L50 57.7z"/><path class="hyst3" d="M32.7 41.7S30 49.1 24 52.2c0 0 9.4-1.1 8.7-10.5zM95.8 41.7s2.7 7.4 8.7 10.5c0 0-9.4-1.1-8.7-10.5zM78.7 55.5s-5.9-6.2-13.8-6.4h.1.1c-8 .2-13.8 6.4-13.8 6.4 6.9-4.8 12.8-4.7 13.8-4.7-.1 0 6.7-.1 13.6 4.7zM71.8 42.5s-3-4.2-7-4.3h.2c-3 .1-6.9 4.3-6.9 4.3 3.4-3.3 6.9-3.2 6.9-3.2s3.3-.1 6.8 3.2zM37.2 73.2s-4.7 2.3-8.1.9H29c-3-1.7-4.5-6.8-4.5-6.8s3 9 12.7 5.9zM92 73.2s4.7 2.3 8.1.9c4-1.7 4.6-6.8 4.6-6.8s-3 9-12.7 5.9z"/><path class="hyst3" d="M42.6 41.2c2.6-.5 6.9-.6 10.3.5 4.3 1.5.8 7 1.7 7.3.9.3 2.1-3.8 10.1-3.4 8.1.4 9 4 10.1 3.4s-1.1-10 11-7.8c0 0-12.7-3.4-12.1 5.8 0 0-7.3-5.6-17.5-.6.1 0 2.7-8.6-13.6-5.2zM86.9 41.2c.2 0 .3.1.4.1.1 0-.1-.1-.4-.1zM86.9 41.2zM39.1 28.9S28.3 42.5 26.7 47.7c-1.6 5.3-2.8 27-4.2 30.1l-5-21.4 9.2-22.3 12.4-5.2zM89.9 28.9s10.8 13.6 12.4 18.8c1.6 5.3 2.8 27 4.2 30.1l5-21.4-9.2-22.3-12.4-5.2z"/><path class="hyst7" d="M89.4 28.9s11.6 9.7 15 20.9c3.4 11.2 2 24.8 4.6 26.5 3.7 2.4 7.9-11.9 9.3-13.4 2.2-2.4 9.5-8.5 10-9.6.5-1.1-14.8-17.8-21.5-21.1-8.1-3.8-18.1-4.1-17.4-3.3z"/><path class="hyst8" d="M99.3 34.9s13.7 17.5 13.5 39.3l5.5-11.2c-.1 0-4.9-14.3-19-28.1z"/><path class="hyst7" d="M39.1 28.9s-11.6 9.7-15 20.9-2 24.8-4.6 26.5c-3.7 2.4-7.9-11.9-9.3-13.4C8 60.5.7 54.4.2 53.3-.3 52.2 15 35.5 21.7 32.2c8.1-3.8 18.1-4.1 17.4-3.3z"/><path class="hyst8" d="M29.2 34.9S15.5 52.4 15.7 74.2L10.3 63s4.8-14.3 18.9-28.1z"/><path class="hyst3" d="M21.8 74.6s1 5.4 2.6 7.1.5-1.3.5-1.3-1.7-.9-1.4-7.8-1.7 2-1.7 2zM107.1 74.6s-1 5.4-2.6 7.1-.5-1.3-.5-1.3 1.7-.9 1.4-7.8 1.7 2 1.7 2z"/><g><circle class="hyst8" cx="54.5" cy="70.5" r=".8"/><circle class="hyst8" cx="49.9" cy="75.3" r=".8"/><circle class="hyst8" cx="48.4" cy="70.5" r=".8"/></g><g><circle class="hyst8" cx="74" cy="70.5" r=".8"/><circle class="hyst8" cx="78.6" cy="75.3" r=".8"/><circle class="hyst8" cx="80.1" cy="70.5" r=".8"/></g></symbol><symbol viewBox="0 0 50 50" id="puppet" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -247)" fill="#fbc02d"><path stroke-width=".283" d="M11.559 249.467h13.587v13.587H11.559zM27.435 265.056h13.587v13.587H27.435zM11.559 281.074h13.587v13.587H11.559z"/><path stroke-width=".256" d="M16.62 251.615l18.305 18.305-3.236 3.236-18.305-18.305z"/><path stroke-width=".256" d="M37.834 271.331L19.53 289.636l-3.237-3.237 18.305-18.304z"/></g></symbol><symbol viewBox="0 0 100 99.999997" id="purescript" xmlns="http://www.w3.org/2000/svg"><path clip-path="url(#SVGID_2_)" d="M98.079 38.548L79.22 19.68l-5.087 5.088L90.447 41.09 74.134 57.41l5.087 5.087 18.858-18.86a3.59 3.59 0 0 0 1.055-2.55 3.578 3.578 0 0 0-1.055-2.54M25.483 42.794l-5.09-5.089L1.53 56.568a3.566 3.566 0 0 0-1.05 2.545c0 .961.373 1.863 1.05 2.542L20.394 80.52l5.089-5.086L9.162 59.113z" fill="#42a5f5" stroke-width="1.192"/><path clip-path="url(#SVGID_2_)" transform="matrix(1.19175 0 0 1.19175 -306.84 -629.047)" fill="#42a5f5" d="M281.841 551.736l6.461 6.037h28.379l-6.461-6.037zM288.302 566.861l-6.463 6.035h28.381l6.463-6.035zM281.838 581.982l6.464 6.035h28.381l-6.463-6.035z"/></symbol><symbol viewBox="0 0 24 24" id="python" xmlns="http://www.w3.org/2000/svg"><path d="M19.14 7.5A2.86 2.86 0 0 1 22 10.36v3.78A2.86 2.86 0 0 1 19.14 17H12c0 .39.32.96.71.96H17v1.68a2.86 2.86 0 0 1-2.86 2.86H9.86A2.86 2.86 0 0 1 7 19.64v-3.75a2.85 2.85 0 0 1 2.86-2.85h5.25a2.85 2.85 0 0 0 2.85-2.86V7.5h1.18m-4.28 11.79c-.4 0-.72.3-.72.89 0 .59.32.71.72.71a.71.71 0 0 0 .71-.71c0-.59-.32-.89-.71-.89m-10-1.79A2.86 2.86 0 0 1 2 14.64v-3.78A2.86 2.86 0 0 1 4.86 8H12c0-.39-.32-.96-.71-.96H7V5.36A2.86 2.86 0 0 1 9.86 2.5h4.28A2.86 2.86 0 0 1 17 5.36v3.75a2.85 2.85 0 0 1-2.86 2.85H8.89a2.85 2.85 0 0 0-2.85 2.86v2.68H4.86M9.14 5.71c.4 0 .72-.3.72-.89 0-.59-.32-.71-.72-.71-.39 0-.71.12-.71.71s.32.89.71.89z"/><path d="M9.264 22.379c-.895-.24-1.581-.799-1.947-1.582-.228-.489-.237-.606-.238-2.957-.001-2.745.057-3.074.666-3.785.193-.226.568-.517.833-.648.47-.23.579-.239 3.839-.288 3.131-.048 3.386-.065 3.814-.264.626-.291 1.07-.687 1.4-1.247.27-.46.278-.522.311-2.29l.034-1.82.932.051c1.075.058 1.504.211 2.098.748.853.77.869.841.869 3.957 0 2.434-.02 2.783-.18 3.075a3.365 3.365 0 0 1-1.337 1.33l-.517.273-3.95.031-3.951.031.068.274c.037.151.164.377.282.503.209.224.262.229 2.433.229h2.22v1.05c0 1.653-.394 2.437-1.54 3.072l-.545.302-2.644.018c-1.455.01-2.782-.018-2.95-.063zm6.12-1.692c.22-.222.253-.325.206-.675-.07-.523-.278-.73-.732-.73-.467 0-.672.217-.735.78-.042.372-.012.496.163.672.3.3.77.28 1.097-.047z" fill="#fc0" stroke="#fc0" stroke-width=".102"/><path d="M9.349 22.38c-.911-.15-1.936-1.074-2.176-1.963-.073-.273-.101-1.279-.079-2.868.033-2.317.047-2.473.27-2.926.13-.263.401-.623.603-.8.674-.592.87-.63 3.484-.675 4.399-.076 4.927-.166 5.705-.967.642-.662.706-.9.774-2.883l.061-1.784.951.055c.523.031 1.11.122 1.304.204.54.225 1.358 1.042 1.472 1.47.153.572.243 3.18.16 4.617-.071 1.23-.093 1.327-.395 1.78-.193.288-.577.647-.966.903l-.647.425-3.922.008c-2.157.004-3.942.028-3.966.052-.115.115.354.82.587.883.14.038 1.181.073 2.314.079l2.06.01v.91c0 1.739-.326 2.446-1.454 3.162l-.631.4-2.543-.011c-1.398-.007-2.733-.043-2.966-.081zm5.98-1.718c.285-.256.313-.328.251-.658-.09-.483-.301-.682-.722-.682-.436 0-.625.193-.715.73-.065.384-.044.453.2.663.358.308.595.295.985-.053z" fill="#fdd835" stroke-width=".102"/><path d="M4.281 17.396c-.88-.215-1.714-.935-2.024-1.747-.149-.389-.168-.804-.142-3.041.027-2.26.054-2.638.215-2.962.259-.519.851-1.092 1.392-1.346.437-.206.632-.217 4.408-.245l3.95-.03-.067-.275a1.367 1.367 0 0 0-.282-.504c-.21-.224-.263-.23-2.433-.23h-2.22l.002-1.143c.003-1.338.157-1.795.84-2.493.746-.763 1.103-.838 4.025-.838 2.961 0 3.28.06 4.067.768.37.333.572.621.728 1.037.201.539.213.735.183 3.072-.035 2.777-.045 2.824-.78 3.598-.787.829-.76.824-4.59.883-3.812.06-3.797.057-4.61.806-.765.706-.917 1.2-.964 3.133l-.04 1.653-.677-.01c-.371-.007-.813-.045-.98-.086zM9.59 5.551c.237-.204.286-.326.286-.72 0-.547-.201-.763-.71-.763-.502 0-.765.248-.765.724 0 .492.141.782.439.902.345.14.444.12.75-.143z" fill="#3c78aa"/></symbol><symbol viewBox="0 0 24 24" id="r" xmlns="http://www.w3.org/2000/svg"><path d="M11.956 4.05c-5.694 0-10.354 3.106-10.354 6.947 0 3.396 3.686 6.212 8.531 6.813v2.205h3.53V17.82c.88-.093 1.699-.259 2.475-.497l1.43 2.692h3.996l-2.402-4.048c1.936-1.263 3.147-3.034 3.147-4.97 0-3.841-4.659-6.947-10.354-6.947m1.584 2.712c4.349 0 7.558 1.45 7.558 4.753 0 1.77-.952 3.013-2.505 3.779a1.081 1.081 0 0 1-.228-.156c-.373-.165-.994-.352-.994-.352s3.085-.227 3.085-3.302-3.23-3.127-3.23-3.127h-7.092v7.413c-2.64-.766-4.462-2.392-4.462-4.255 0-2.63 3.52-4.753 7.868-4.753m.156 4.12h2.143s.983-.05.983.974c0 1.004-.983 1.004-.983 1.004h-2.143v-1.977m-.031 4.566h.952c.186 0 .28.052.445.207.135.103.28.3.404.476-.57.073-1.17.104-1.801.104z" fill="#1976d2" stroke-width="1.035"/></symbol><symbol viewBox="0 0 24 24" id="raml" xmlns="http://www.w3.org/2000/svg"><path d="M5 3h2v2H5v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5h2v2H5c-1.07-.27-2-.9-2-2v-4a2 2 0 0 0-2-2H0v-2h1a2 2 0 0 0 2-2V5a2 2 0 0 1 2-2m14 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3h2m-7 12a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m-4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="razor" xmlns="http://www.w3.org/2000/svg"><path d="M15.45 11.91c-.11-2.21-1.75-3.54-3.73-3.54h-.08c-2.29 0-3.55 1.8-3.55 3.84 0 2.29 1.53 3.74 3.54 3.74 2.25 0 3.72-1.65 3.83-3.59m-3.81-5.97c1.53 0 2.97.68 4.02 1.74 0-.51.33-.89.83-.89h.11c.74 0 .89.7.89.92v7.9c-.04.52.54.78.87.44 1.27-1.29 2.78-6.69-.79-9.81-3.33-2.92-7.8-2.44-10.18-.8-2.52 1.74-4.14 5.61-2.57 9.22 1.71 3.95 6.61 5.13 9.52 3.95 1.48-.59 2.15 1.4.65 2.05-2.34.99-8.77.89-11.78-4.32-2.03-3.52-1.93-9.71 3.46-12.92C10.81 1.42 16.24 2.1 19.5 5.5c3.45 3.6 3.25 10.3-.1 12.91-1.51 1.18-3.76.03-3.74-1.7l-.02-.56a5.611 5.611 0 0 1-3.99 1.66C8.63 17.81 6 15.15 6 12.13c0-3.05 2.63-5.74 5.65-5.74z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="react" xmlns="http://www.w3.org/2000/svg"><path d="M12 10.11c1.03 0 1.87.84 1.87 1.89 0 1-.84 1.85-1.87 1.85-1.03 0-1.87-.85-1.87-1.85 0-1.05.84-1.89 1.87-1.89M7.37 20c.63.38 2.01-.2 3.6-1.7-.52-.59-1.03-1.23-1.51-1.9a22.7 22.7 0 0 1-2.4-.36c-.51 2.14-.32 3.61.31 3.96m.71-5.74l-.29-.51c-.11.29-.22.58-.29.86.27.06.57.11.88.16l-.3-.51m6.54-.76l.81-1.5-.81-1.5c-.3-.53-.62-1-.91-1.47C13.17 9 12.6 9 12 9c-.6 0-1.17 0-1.71.03-.29.47-.61.94-.91 1.47L8.57 12l.81 1.5c.3.53.62 1 .91 1.47.54.03 1.11.03 1.71.03.6 0 1.17 0 1.71-.03.29-.47.61-.94.91-1.47M12 6.78c-.19.22-.39.45-.59.72h1.18c-.2-.27-.4-.5-.59-.72m0 10.44c.19-.22.39-.45.59-.72h-1.18c.2.27.4.5.59.72M16.62 4c-.62-.38-2 .2-3.59 1.7.52.59 1.03 1.23 1.51 1.9.82.08 1.63.2 2.4.36.51-2.14.32-3.61-.32-3.96m-.7 5.74l.29.51c.11-.29.22-.58.29-.86-.27-.06-.57-.11-.88-.16l.3.51m1.45-7.05c1.47.84 1.63 3.05 1.01 5.63 2.54.75 4.37 1.99 4.37 3.68 0 1.69-1.83 2.93-4.37 3.68.62 2.58.46 4.79-1.01 5.63-1.46.84-3.45-.12-5.37-1.95-1.92 1.83-3.91 2.79-5.38 1.95-1.46-.84-1.62-3.05-1-5.63-2.54-.75-4.37-1.99-4.37-3.68 0-1.69 1.83-2.93 4.37-3.68-.62-2.58-.46-4.79 1-5.63 1.47-.84 3.46.12 5.38 1.95 1.92-1.83 3.91-2.79 5.37-1.95M17.08 12c.34.75.64 1.5.89 2.26 2.1-.63 3.28-1.53 3.28-2.26 0-.73-1.18-1.63-3.28-2.26-.25.76-.55 1.51-.89 2.26M6.92 12c-.34-.75-.64-1.5-.89-2.26-2.1.63-3.28 1.53-3.28 2.26 0 .73 1.18 1.63 3.28 2.26.25-.76.55-1.51.89-2.26m9 2.26l-.3.51c.31-.05.61-.1.88-.16-.07-.28-.18-.57-.29-.86l-.29.51m-2.89 4.04c1.59 1.5 2.97 2.08 3.59 1.7.64-.35.83-1.82.32-3.96-.77.16-1.58.28-2.4.36-.48.67-.99 1.31-1.51 1.9M8.08 9.74l.3-.51c-.31.05-.61.1-.88.16.07.28.18.57.29.86l.29-.51m2.89-4.04C9.38 4.2 8 3.62 7.37 4c-.63.35-.82 1.82-.31 3.96a22.7 22.7 0 0 1 2.4-.36c.48-.67.99-1.31 1.51-1.9z" fill="#00bcd4"/></symbol><symbol viewBox="0 0 24 24" id="readme" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="reason" xmlns="http://www.w3.org/2000/svg"><path d="M3 3v18h18V3H3zm5.119 8.993h2.798c.382 0 .71.025.985.075.275.05.534.159.774.326.244.168.435.386.577.654.145.265.218.598.218 1 0 .552-.112 1.001-.335 1.35-.22.348-.536.638-.947.87l2.16 3.203H12.31l-1.763-2.742h-.77v2.742H8.12v-7.478zm6.594 0h4.676v1.447h-3.018v1.29h2.802v1.447h-2.802v1.848h3.018v1.446h-4.676v-7.478zM9.778 13.37v2.014h.513c.266 0 .49-.014.67-.044.18-.03.329-.1.45-.207a.96.96 0 0 0 .253-.34c.055-.128.082-.297.082-.508 0-.187-.034-.35-.1-.483a.698.698 0 0 0-.343-.317 1.086 1.086 0 0 0-.395-.095 6.012 6.012 0 0 0-.526-.02h-.604z" fill="#f44336" stroke-width="1.067"/></symbol><symbol viewBox="0 0 172 193" id="restql" xmlns="http://www.w3.org/2000/svg"><title>Group</title><g transform="translate(14.767 16.713) scale(.82795)" fill="none"><path d="M171.39 55.799c-.975-6.147-4.673-11.642-10.15-14.805L96.381 3.546C93.217 1.72 89.615.756 85.964.756s-7.253.964-10.415 2.788L10.69 40.992A20.896 20.896 0 0 0 .272 59.035v74.89a20.894 20.894 0 0 0 10.416 18.042l64.859 37.446c3.165 1.827 6.767 2.791 10.417 2.791s7.252-.964 10.415-2.79l64.859-37.445c5.479-3.166 9.178-8.66 10.152-14.808zm-16.516 85.147L90.017 178.39a8.104 8.104 0 0 1-8.108 0l-64.857-37.444a8.109 8.109 0 0 1-4.053-7.021v-74.89a8.109 8.109 0 0 1 4.053-7.021l64.857-37.446c1.254-.725 2.654-1.086 4.054-1.086s2.8.361 4.054 1.086l64.857 37.446a8.106 8.106 0 0 1 4.053 7.021v74.89a8.109 8.109 0 0 1-4.053 7.021z" fill="#83e8c2"/><path d="M158.93 59.035a8.109 8.109 0 0 0-4.053-7.021L90.02 14.568c-1.254-.725-2.654-1.086-4.054-1.086s-2.8.361-4.054 1.086L17.055 52.014a8.106 8.106 0 0 0-4.053 7.021v74.89a8.109 8.109 0 0 0 4.053 7.021l64.857 37.444a8.104 8.104 0 0 0 8.108 0l64.857-37.444a8.109 8.109 0 0 0 4.053-7.021zm-46.766 31.681c.119-.069.242-.118.365-.149.044-.012.088-.01.131-.018.076-.012.152-.029.228-.029l.015.001c.02.001.038.005.059.006.093.005.184.019.273.04l.1.03c.077.025.15.057.223.095.028.014.057.027.084.043.094.057.184.122.263.199.007.008.013.017.021.024.07.071.133.15.188.235.018.029.033.059.05.09.04.072.072.148.099.229a1.512 1.512 0 0 1 .081.46v16.209l-3.278 1.893a1.548 1.548 0 0 0-.678.83 1.533 1.533 0 0 0-.098.514v3.785l-14.038 8.104-.01.004a1.55 1.55 0 0 1-.354.146c-.045.012-.09.011-.135.018-.074.012-.15.029-.225.029l-.014-.001c-.02-.001-.039-.005-.059-.006a1.463 1.463 0 0 1-.273-.041c-.034-.008-.066-.019-.1-.03a1.318 1.318 0 0 1-.223-.094c-.029-.015-.057-.027-.084-.044a1.45 1.45 0 0 1-.263-.198c-.009-.008-.015-.019-.023-.027a1.495 1.495 0 0 1-.185-.232c-.019-.029-.034-.06-.051-.09a1.422 1.422 0 0 1-.098-.229 1.702 1.702 0 0 1-.033-.101 1.487 1.487 0 0 1-.048-.358l-.001-.002v-20.053a1.446 1.446 0 0 1 .727-1.255zM85.24 31.369a1.449 1.449 0 0 1 1.452 0l45.741 26.41a1.45 1.45 0 0 1 0 2.512l-17.366 10.027a1.457 1.457 0 0 1-1.452 0l-15.49-8.943 1.727-.996a1.552 1.552 0 0 0 0-2.688l-13.111-7.57c-.239-.139-.508-.207-.775-.207s-.535.068-.775.207l-3.278 1.893-14.038-8.104a1.451 1.451 0 0 1 0-2.513zM57.59 47.558c.251 0 .501.065.726.194l15.489 8.942-1.727.997a1.552 1.552 0 0 0 0 2.688l1.727.996-15.488 8.943a1.457 1.457 0 0 1-1.452 0L39.499 60.291a1.45 1.45 0 0 1 0-2.512l17.366-10.027c.225-.129.475-.194.725-.194zm-9.56 92.328c-.241 0-.489-.062-.724-.196l-17.365-10.026a1.45 1.45 0 0 1-.726-1.256V75.59c0-.847.694-1.453 1.452-1.453.242 0 .49.062.724.197l17.366 10.025c.449.26.726.738.726 1.257v17.886l-1.727-.997a1.552 1.552 0 0 0-2.327 1.344v15.139c0 .555.295 1.067.775 1.344l3.278 1.894v16.209a1.45 1.45 0 0 1-1.452 1.451zm29.828 14.929a1.452 1.452 0 0 1-2.177 1.257l-17.365-10.026a1.452 1.452 0 0 1-.726-1.257v-17.885l1.726.996c.25.145.515.211.773.211.811 0 1.554-.648 1.554-1.555v-1.993l15.489 8.942c.449.26.726.738.726 1.257zm0-32.768c0 .127-.02.246-.049.36-.009.035-.021.067-.032.101-.026.08-.059.157-.099.229-.017.03-.032.061-.05.09a1.48 1.48 0 0 1-.188.235l-.021.025a1.51 1.51 0 0 1-.264.199c-.026.016-.055.028-.082.043a1.597 1.597 0 0 1-.324.124 1.362 1.362 0 0 1-.278.041c-.018.001-.036.006-.055.006l-.015.001c-.077 0-.155-.018-.233-.03-.043-.007-.084-.005-.125-.017a1.484 1.484 0 0 1-.366-.149l-14.035-8.104v-3.784a1.545 1.545 0 0 0-.776-1.343l-3.276-1.892V91.976c0-.127.02-.246.049-.361.009-.034.021-.066.032-.1a1.33 1.33 0 0 1 .099-.229c.017-.03.032-.062.051-.091.054-.084.116-.163.187-.234l.021-.025c.079-.076.168-.142.263-.199.027-.016.056-.029.084-.043a1.476 1.476 0 0 1 .601-.166c.019 0 .036-.005.055-.005l.015-.001c.078 0 .157.018.236.03.04.007.081.005.122.017.124.031.246.08.366.149l17.361 10.023a1.456 1.456 0 0 1 .726 1.259zm-9.984-45.373a1.448 1.448 0 0 1-.544-.55 1.466 1.466 0 0 1 0-1.413c.121-.219.303-.41.544-.55l14.038-8.104 3.277 1.892c.48.276 1.071.276 1.551 0l3.278-1.893 14.038 8.105a1.45 1.45 0 0 1 0 2.513L86.691 86.7a1.447 1.447 0 0 1-1.452 0zm74.842 51.733c0 .518-.276.997-.726 1.256l-45.741 26.409a1.452 1.452 0 0 1-2.177-1.257v-20.053c0-.519.277-.997.727-1.257l15.488-8.941v1.992c0 .906.743 1.555 1.553 1.555.26 0 .523-.066.774-.21l13.11-7.57a1.55 1.55 0 0 0 .776-1.344v-3.784l14.038-8.105a1.452 1.452 0 0 1 2.177 1.257v20.052zm0-32.764c0 .519-.276.997-.726 1.256l-15.489 8.943v-1.993c0-.906-.744-1.554-1.554-1.554a1.519 1.519 0 0 0-.773.21l-1.727.996V85.616c0-.519.277-.997.727-1.257l17.365-10.025c.234-.135.482-.197.724-.197.758 0 1.453.606 1.453 1.453z" fill="#111d5a"/><g fill="#83e8c2"><path d="M59.402 90.568zM94.485 123.06zM94.771 123.29zM77.775 122.51zM77.072 123.33zM77.418 123.09zM77.856 122.05zM76.749 123.45zM94.119 122.41zM77.131 133.51l-15.489-8.942v1.993c0 .906-.743 1.555-1.554 1.555a1.53 1.53 0 0 1-.773-.211l-1.726-.996v17.885c0 .519.276.997.726 1.257l17.365 10.026a1.452 1.452 0 0 0 2.177-1.257v-20.053a1.454 1.454 0 0 0-.726-1.257zM94.25 122.74zM110.28 111.42zM94.494 100.98c.088-.089.189-.168.303-.232l17.365-10.026-17.365 10.026a1.392 1.392 0 0 0-.303.232zM77.627 122.83zM58.027 90.936zM58.374 90.693zM59.044 90.521l-.015.001c.083-.001.167.015.251.029-.079-.012-.158-.03-.236-.03zM57.819 91.195zM58.696 90.568zM57.589 91.977zM76.043 123.46zM57.67 91.516zM75.677 123.31l-14.035-8.11zM76.401 123.5l.015-.001c-.082.001-.166-.016-.248-.029.078.012.156.03.233.03zM112.16 90.716zM77.662 101.27zM113.64 90.734zM96.237 123.31zM113.33 90.597zM112.89 90.52c-.075 0-.151.018-.228.029.081-.014.162-.029.242-.028l-.014-.001zM141.26 74.137c-.241 0-.489.062-.724.197l-17.365 10.025c-.449.26-.727.738-.727 1.257v17.885l1.727-.996c.25-.145.515-.211.773-.21.81 0 1.554.647 1.554 1.554v1.993l15.489-8.943a1.45 1.45 0 0 0 .726-1.256V75.59c0-.847-.695-1.453-1.453-1.453zM112.96 90.526zM95.523 123.5c.074 0 .15-.018.225-.029-.08.013-.159.028-.238.028l.013.001zM95.451 123.5zM85.238 86.7zM95.078 123.43zM141.26 106.9c-.241 0-.489.062-.724.196l-14.038 8.105v3.784c0 .555-.296 1.067-.776 1.344l-13.11 7.57c-.251.144-.515.21-.774.21-.81 0-1.553-.648-1.553-1.555v-1.992l-15.488 8.941c-.449.26-.727.738-.727 1.257v20.053a1.452 1.452 0 0 0 2.177 1.257l45.741-26.409a1.45 1.45 0 0 0 .726-1.256v-20.053a1.454 1.454 0 0 0-1.454-1.452zM67.871 41.396a1.451 1.451 0 0 0 0 2.513l14.038 8.104 3.278-1.893c.24-.139.508-.207.775-.207s.536.068.775.207l13.111 7.57a1.552 1.552 0 0 1 0 2.688l-1.727.996 15.49 8.943a1.457 1.457 0 0 0 1.452 0l17.366-10.027a1.45 1.45 0 0 0 0-2.512l-45.741-26.41a1.449 1.449 0 0 0-1.452 0zM39.497 57.779a1.45 1.45 0 0 0 0 2.512l17.366 10.027a1.457 1.457 0 0 0 1.452 0l15.488-8.943-1.727-.996a1.552 1.552 0 0 1 0-2.688l1.727-.997-15.489-8.942a1.458 1.458 0 0 0-1.451 0zM49.481 138.43v-16.209l-3.278-1.894a1.55 1.55 0 0 1-.775-1.344v-15.139c0-.906.743-1.555 1.554-1.554.259 0 .523.065.773.21l1.727.997V85.611a1.45 1.45 0 0 0-.726-1.257L31.39 74.33a1.436 1.436 0 0 0-.724-.197c-.758 0-1.452.606-1.452 1.453v52.817c0 .518.276.997.726 1.256l17.365 10.026a1.45 1.45 0 0 0 2.176-1.255zM114.34 108.18l-3.278 1.893 3.278-1.893V91.971zM114.11 91.193zM114.16 91.283z"/></g><g fill="#de5941"><path d="M94.494 100.98a1.45 1.45 0 0 0-.424 1.023v20.053l.001.002c0 .126.02.244.048.358.01.034.021.066.033.101.026.08.059.156.098.229.017.03.032.061.051.09.055.084.115.162.185.232.009.009.015.02.023.027.079.077.169.142.263.198.027.017.055.029.084.044a1.46 1.46 0 0 0 .596.165c.02.001.039.005.059.006.079 0 .158-.016.238-.028.045-.007.09-.006.135-.018.119-.031.238-.08.354-.146l.01-.004 14.038-8.104v-3.785c0-.18.04-.35.098-.514.122-.343.353-.643.678-.83l3.278-1.893V91.977c0-.127-.021-.246-.049-.361-.009-.033-.021-.065-.032-.099a1.266 1.266 0 0 0-.099-.229c-.017-.031-.032-.061-.05-.09a1.425 1.425 0 0 0-.188-.235l-.021-.024a1.41 1.41 0 0 0-.263-.199c-.027-.016-.056-.029-.084-.043a1.509 1.509 0 0 0-.323-.125 1.591 1.591 0 0 0-.273-.04c-.021-.001-.039-.005-.059-.006-.08-.001-.161.015-.242.028-.043.008-.087.006-.131.018-.123.031-.246.08-.365.149l-17.365 10.026a1.447 1.447 0 0 0-.302.233zM77.13 100.74L59.769 90.717a1.424 1.424 0 0 0-.366-.149c-.041-.012-.082-.01-.122-.017-.084-.015-.168-.03-.251-.029-.019 0-.036.005-.055.005-.095.005-.188.02-.278.041-.034.009-.065.02-.099.03a1.406 1.406 0 0 0-.224.095c-.028.014-.057.027-.084.043a1.515 1.515 0 0 0-.263.199l-.021.025c-.07.071-.133.15-.187.234-.019.029-.034.061-.051.091-.04.073-.072.149-.099.229a1.463 1.463 0 0 0-.081.461v16.206l3.276 1.892a1.547 1.547 0 0 1 .776 1.343v3.784l14.035 8.104c.119.068.242.117.366.149.041.012.082.01.125.017.082.014.166.03.248.029.019 0 .037-.005.055-.006.095-.004.188-.019.278-.041.034-.008.065-.019.099-.029.077-.025.152-.058.225-.095.027-.015.056-.027.082-.043.095-.058.185-.123.264-.199l.021-.025c.07-.071.133-.15.188-.235.018-.029.033-.06.05-.09.04-.072.072-.149.099-.229a1.448 1.448 0 0 0 .081-.461v-20.047a1.456 1.456 0 0 0-.726-1.259zM86.689 86.7l17.365-10.026a1.45 1.45 0 0 0 0-2.513l-14.038-8.105-3.278 1.893a1.556 1.556 0 0 1-1.551 0l-3.277-1.892-14.038 8.104c-.241.14-.423.331-.544.55a1.466 1.466 0 0 0 0 1.413c.121.218.303.41.544.55L85.238 86.7a1.447 1.447 0 0 0 1.451 0z"/></g></g></symbol><symbol viewBox="0 0 24 24" id="riot" xmlns="http://www.w3.org/2000/svg"><defs><path d="M13.26 3.04l.58.05.54.07.52.09.49.11.46.13.44.14.41.16.39.17.36.19.33.21.32.22.29.23.26.25.22.22.2.22.19.24.17.24.15.25.15.26.12.27.12.28.1.29.08.31.07.31.05.32.04.34.02.35.01.37v.05l-.02.51-.05.49-.09.48-.13.45-.15.43-.19.4-.22.39-.26.37-.28.34-.31.33-.33.3-.37.28-.39.27-.41.24-.44.22L21 21h-7.04l-3.48-5.14H9.17V21H3V3h9.01l.64.01.61.03zm-4.09 8.52h2.66l.99-.11.75-.35.47-.55.16-.74v-.05l-.17-.75-.47-.54-.74-.32-.96-.11H9.17v3.52z" id="ija"/></defs><use xlink:href="#ija" fill="#ff1744"/><use xlink:href="#ija" fill-opacity="0" stroke="#000" stroke-opacity="0"/></symbol><symbol viewBox="0 0 24 24" id="robot" xmlns="http://www.w3.org/2000/svg"><path d="M12.05 2.804a1.787 1.787 0 0 1 1.788 1.788c0 .661-.357 1.242-.893 1.546v1.135h.893a6.256 6.256 0 0 1 6.256 6.256h.894a.894.894 0 0 1 .893.893v2.681a.894.894 0 0 1-.893.894h-.894v.894a1.787 1.787 0 0 1-1.787 1.787H5.795a1.787 1.787 0 0 1-1.787-1.787v-.894h-.894a.894.894 0 0 1-.894-.894v-2.68a.894.894 0 0 1 .894-.894h.894a6.256 6.256 0 0 1 6.255-6.256h.894V6.138a1.773 1.773 0 0 1-.894-1.546 1.787 1.787 0 0 1 1.788-1.788m-4.022 9.83a2.234 2.234 0 0 0-2.234 2.235 2.234 2.234 0 0 0 2.234 2.234 2.234 2.234 0 0 0 2.234-2.234 2.234 2.234 0 0 0-2.234-2.234m8.043 0a2.234 2.234 0 0 0-2.234 2.234 2.234 2.234 0 0 0 2.234 2.234 2.234 2.234 0 0 0 2.235-2.234 2.234 2.234 0 0 0-2.235-2.234z" fill="#ff5722" stroke-width=".894"/></symbol><symbol viewBox="100 100 800 800" id="rollup" xmlns="http://www.w3.org/2000/svg"><style>.ilst0{fill:url(#ilXMLID_4_)}.ilst1{fill:url(#ilXMLID_5_)}.ilst2{fill:url(#ilXMLID_8_)}.ilst3{fill:url(#ilXMLID_9_)}.ilst4{fill:url(#ilXMLID_11_)}.ilst5{opacity:.3;fill:url(#ilXMLID_16_)}</style><g id="ilXMLID_14_" transform="translate(-54.117 -62.353) scale(1.1129)"><linearGradient id="ilXMLID_4_" x1="444.47" x2="598.47" y1="526.05" y2="562.05" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6533" offset="0"/><stop stop-color="#FF5633" offset=".157"/><stop stop-color="#FF4333" offset=".434"/><stop stop-color="#FF3733" offset=".714"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_15_" class="ilst0" d="M721 410c0-33.6-8.8-65.1-24.3-92.4-41.1-42.3-130.5-52.1-152.7-.2-22.8 53.2 38.3 112.4 65 107.7 34-6-6-84-6-84 52 98 40 68-54 158S359 779 345 787c-.6.4-1.2.7-1.9 1h368.7c6.5 0 10.7-6.9 7.8-12.7l-96.4-190.8c-2.1-4.1-.6-9.2 3.4-11.5C683 540.6 721 479.8 721 410z" fill="url(#ilXMLID_4_)"/></g><g id="ilXMLID_2_" transform="translate(-54.117 -62.353) scale(1.1129)"><linearGradient id="ilXMLID_5_" x1="420.38" x2="696.38" y1="475" y2="689" gradientUnits="userSpaceOnUse"><stop stop-color="#BF3338" offset="0"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_10_" class="ilst1" d="M721 410c0-33.6-8.8-65.1-24.3-92.4-41.1-42.3-130.5-52.1-152.7-.2-22.8 53.2 38.3 112.4 65 107.7 34-6-6-84-6-84 52 98 40 68-54 158S359 779 345 787c-.6.4-1.2.7-1.9 1h368.7c6.5 0 10.7-6.9 7.8-12.7l-96.4-190.8c-2.1-4.1-.6-9.2 3.4-11.5C683 540.6 721 479.8 721 410z" fill="url(#ilXMLID_5_)"/></g><linearGradient id="ilXMLID_8_" x1="429.39" x2="469.39" y1="517.16" y2="559.16" gradientTransform="translate(-54.117 -62.353) scale(1.1129)" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6533" offset="0"/><stop stop-color="#FF5633" offset=".157"/><stop stop-color="#FF4333" offset=".434"/><stop stop-color="#FF3733" offset=".714"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_3_" class="ilst2" d="M329.82 813.46c15.58-8.903 122.41-220.34 227.02-320.5s117.96-66.771 60.094-175.83c0 0-221.46 310.49-301.58 464.06" fill="url(#ilXMLID_8_)" stroke-width="1.113"/><g id="ilXMLID_7_" transform="translate(-54.117 -62.353) scale(1.1129)"><linearGradient id="ilXMLID_9_" x1="502.11" x2="490.11" y1="589.46" y2="417.46" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6533" offset="0"/><stop stop-color="#FF5633" offset=".157"/><stop stop-color="#FF4333" offset=".434"/><stop stop-color="#FF3733" offset=".714"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_12_" class="ilst3" d="M373 537c134.4-247.1 152-272 222-272 36.8 0 73.9 16.6 97.9 46.1-32.7-52.7-90.6-88-156.9-89H307.7c-4.8 0-8.7 3.9-8.7 8.7V691c13.6-35.1 36.7-85.3 74-154z" fill="url(#ilXMLID_9_)"/></g><linearGradient id="ilXMLID_11_" x1="450.12" x2="506.94" y1="514.21" y2="552.85" gradientTransform="translate(-54.117 -62.353) scale(1.1129)" gradientUnits="userSpaceOnUse"><stop stop-color="#FBB040" offset="0"/><stop stop-color="#FB8840" offset="1"/></linearGradient><path id="ilXMLID_6_" class="ilst4" d="M556.84 492.96c-104.61 100.16-211.44 311.6-227.02 320.5s-41.732 10.016-55.643-5.564c-14.801-16.582-37.837-43.401 86.802-272.65 149.57-274.99 169.15-302.7 247.05-302.7 40.953 0 82.24 18.473 108.95 51.302 1.447 2.337 2.893 4.785 4.34 7.233-45.738-47.074-145.23-57.98-169.93-.222-25.373 59.204 42.622 125.08 72.335 119.85 37.837-6.677-6.677-93.48-6.677-93.48 57.757 108.95 44.403 75.563-60.205 175.72z" fill="url(#ilXMLID_11_)" stroke-width="1.113"/><linearGradient id="ilXMLID_16_" x1="508.33" x2="450.33" y1="295.76" y2="933.76" gradientTransform="translate(-54.117 -62.353) scale(1.1129)" gradientUnits="userSpaceOnUse"><stop stop-color="#FFF" offset="0"/><stop stop-color="#FFF" stop-opacity="0" offset="1"/></linearGradient><path id="ilXMLID_13_" class="ilst5" d="M373.22 547.49c149.57-274.99 169.15-302.7 247.05-302.7 33.719 0 67.661 12.575 93.48 35.277-26.708-30.492-66.326-47.519-105.72-47.519-77.9 0-97.486 27.71-247.05 302.7-124.64 229.25-101.6 256.07-86.802 272.65 2.114 2.337 4.563 4.34 7.122 6.01-13.02-18.919-18.807-62.877 91.922-266.42z" fill="url(#ilXMLID_16_)" opacity=".3" stroke-width="1.113"/></symbol><symbol viewBox="0 0 24 24" id="ruby" xmlns="http://www.w3.org/2000/svg"><path d="M16 9h3l-5 7m-4-7h4l-2 8M5 9h3l2 7m5-12h2l2 3h-3m-5-3h2l1 3h-4M7 4h2L8 7H5m1-5L2 8l10 14L22 8l-4-6H6z" fill="#f44336"/></symbol><symbol viewBox="0 0 144 144" id="rust" xmlns="http://www.w3.org/2000/svg"><path d="M68.252 26.206a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0M25.766 58.451a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m84.97.166a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m-74.661 4.88a3.252 3.252 0 0 0 1.651-4.29l-1.58-3.574h6.214v28.01H29.823a43.847 43.847 0 0 1-1.42-16.738zm25.994.688v-8.256h14.798c.764 0 5.397.883 5.397 4.347 0 2.877-3.553 3.908-6.475 3.908zm-20.203 44.452a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m52.769.166a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m1.101-8.076a3.246 3.246 0 0 0-3.856 2.498l-1.787 8.342a43.847 43.847 0 0 1-36.566-.175l-1.787-8.342a3.246 3.246 0 0 0-3.854-2.497l-7.365 1.581a43.847 43.847 0 0 1-3.808-4.488h35.834c.406 0 .676-.074.676-.443V84.527c0-.369-.27-.442-.676-.442h-10.48V76.05h11.335c1.035 0 5.532.296 6.97 6.045.45 1.768 1.44 7.519 2.116 9.36.674 2.065 3.417 6.19 6.34 6.19h18.501a43.847 43.847 0 0 1-4.06 4.7zm19.898-33.468a43.847 43.847 0 0 1 .093 7.612h-4.499c-.45 0-.631.296-.631.737v2.066c0 4.863-2.742 5.92-5.145 6.19-2.288.258-4.825-.958-5.138-2.358-1.35-7.593-3.6-9.214-7.152-12.016 4.409-2.8 8.996-6.93 8.996-12.457 0-5.97-4.092-9.729-6.881-11.572-3.914-2.58-8.246-3.096-9.415-3.096H39.336A43.847 43.847 0 0 1 63.867 28.52l5.484 5.753a3.243 3.243 0 0 0 4.59.105l6.137-5.869a43.847 43.847 0 0 1 30.017 21.38l-4.201 9.487a3.256 3.256 0 0 0 1.652 4.29zm10.477.154l-.143-1.467 4.327-4.036c.88-.82.55-2.472-.574-2.891l-5.532-2.068-.433-1.428 3.45-4.792c.704-.974.058-2.53-1.127-2.724l-5.833-.949-.7-1.31 2.45-5.38c.502-1.095-.43-2.496-1.636-2.45l-5.92.206-.935-1.135 1.36-5.766c.275-1.17-.913-2.36-2.084-2.085l-5.765 1.359-1.136-.935.207-5.92c.046-1.198-1.357-2.135-2.45-1.637l-5.379 2.452-1.31-.703-.95-5.833c-.193-1.183-1.75-1.83-2.723-1.128l-4.796 3.45-1.425-.432-2.068-5.532c-.42-1.127-2.072-1.452-2.89-.576l-4.036 4.33-1.467-.143-3.117-5.036c-.63-1.02-2.318-1.02-2.946 0l-3.117 5.036-1.467.143-4.037-4.33c-.819-.876-2.47-.551-2.89.576l-2.069 5.532-1.426.432-4.795-3.45c-.974-.703-2.53-.055-2.723 1.128l-.951 5.833-1.31.703-5.379-2.452c-1.093-.5-2.496.439-2.45 1.637l.206 5.92-1.136.935-5.765-1.36c-1.171-.272-2.36.915-2.086 2.086l1.358 5.766-.933 1.135-5.92-.206c-1.193-.035-2.134 1.355-1.637 2.45l2.453 5.38-.703 1.31-5.832.949c-1.185.192-1.827 1.75-1.128 2.724l3.45 4.792-.433 1.428-5.532 2.068c-1.123.42-1.452 2.07-.574 2.891l4.328 4.036-.143 1.467-5.035 3.116c-1.02.63-1.02 2.318 0 2.946l5.035 3.117.143 1.467-4.328 4.037c-.878.818-.549 2.468.574 2.89l5.532 2.068.433 1.428-3.45 4.793c-.701.976-.056 2.532 1.129 2.723l5.831.948.703 1.312-2.453 5.378c-.5 1.093.444 2.5 1.638 2.451l5.917-.207.935 1.136-1.358 5.768c-.275 1.168.915 2.355 2.086 2.08l5.765-1.357 1.137.932-.207 5.921c-.046 1.199 1.357 2.136 2.45 1.636l5.379-2.45 1.31.702.95 5.83c.193 1.187 1.75 1.829 2.725 1.13l4.792-3.453 1.427.435 2.069 5.53c.42 1.123 2.072 1.454 2.89.574l4.037-4.328 1.467.146 3.117 5.035c.628 1.016 2.316 1.018 2.946 0l3.117-5.035 1.467-.146 4.036 4.328c.818.88 2.47.549 2.89-.574l2.068-5.53 1.428-.435 4.793 3.453c.974.699 2.53.055 2.722-1.13l.952-5.83 1.31-.703 5.378 2.451c1.093.5 2.493-.435 2.45-1.636l-.206-5.92 1.135-.933 5.765 1.357c1.171.275 2.36-.912 2.085-2.08l-1.358-5.768.932-1.136 5.92.207c1.194.048 2.138-1.358 1.636-2.451l-2.45-5.378.7-1.312 5.833-.948c1.187-.19 1.831-1.747 1.127-2.723l-3.45-4.793.433-1.428 5.532-2.068c1.125-.422 1.454-2.072.574-2.89l-4.327-4.037.143-1.467 5.035-3.117c1.02-.628 1.021-2.315.001-2.946z" fill="#ff7043" stroke-width="1.146"/></symbol><symbol viewBox="0 0 500 500" id="sass" xmlns="http://www.w3.org/2000/svg"><path d="M422.676 96.573c-12.192-47.839-91.508-63.557-166.575-36.892-44.68 15.877-93.029 40.786-127.81 73.311-41.349 38.675-47.943 72.328-45.216 86.395 9.583 49.622 77.585 82.069 105.535 106.126v.144c-8.246 4.05-68.565 34.584-82.684 65.799-14.893 32.932 2.372 56.556 13.804 59.742 35.424 9.859 71.764-7.866 91.311-37.01 18.853-28.12 17.28-64.422 9.086-82.487 11.3-2.976 24.476-4.314 41.218-2.36 47.248 5.52 56.517 35.017 54.747 47.366-1.77 12.35-11.681 19.14-14.998 21.186-3.317 2.045-4.326 2.766-4.05 4.287.405 2.215 1.94 2.137 4.758 1.652 3.894-.656 24.804-10.042 25.709-32.828 1.14-28.933-26.587-61.302-75.684-60.45-20.216.354-32.933 2.268-42.123 5.69-.681-.774-1.363-1.547-2.084-2.307-30.35-32.382-86.46-55.285-84.088-98.824.866-15.823 6.372-57.5 107.817-108.052 83.104-41.415 149.637-30.009 161.135-4.76 16.427 36.08-35.554 103.137-121.858 112.812-32.88 3.684-50.198-9.059-54.498-13.804-4.536-4.995-5.204-5.218-6.909-4.287-2.753 1.533-1.01 5.938 0 8.574 2.583 6.712 13.15 18.603 31.176 24.515 15.863 5.205 54.459 8.063 101.156-9.99 52.283-20.255 93.12-76.523 81.125-123.548zM200.213 340.34c3.92 14.5 3.487 28.016-.564 40.248a65.289 65.289 0 0 1-3.225 7.97c-3.12 6.477-7.316 12.534-12.442 18.132-15.653 17.069-37.507 23.532-46.88 18.092-10.122-5.874-5.048-29.944 13.083-49.11 19.52-20.636 47.602-33.903 47.602-33.903l-.039-.079 2.465-1.35z" fill="#ec407a" stroke="#ec407a" stroke-width="16.286552999999998"/></symbol><symbol viewBox="0 0 300 300" id="sbt" xmlns="http://www.w3.org/2000/svg"><path d="M105.46 209.517c-7.875 0-13.452-7.521-13.452-15.37v-.327c0-7.848 5.578-13.735 13.452-13.735h164.05c1.476-4.905 2.625-11.446 3.281-17.986h-137.81c-7.875 0-14.273-6.05-14.273-13.898s6.398-13.898 14.273-13.898h137.31c-.82-6.54-1.969-13.081-3.773-17.986h-104.01c-7.875 0-14.273-6.05-14.273-13.898s6.398-13.898 14.273-13.898h91.87c-21.327-37.607-60.864-61.315-106.14-61.315-67.918 0-123.04 54.448-123.04 122.3 0 67.856 55.122 123.28 123.04 123.28 46.59 0 87.112-25.507 107.95-63.114h-152.73z" fill="#0277bd" stroke-width="1.638"/></symbol><symbol viewBox="0 0 256 256" id="scala" xmlns="http://www.w3.org/2000/svg"><path fill="#f44336" fill-rule="evenodd" stroke-width=".3" d="M59.607 50.647l149.097-21.982v49.488L59.607 100.135zM59.593 114.08L208.69 92.098v49.488L59.593 163.568zM59.587 177.358l149.097-21.982v49.488L59.587 226.846z"/><path fill="#f44336" fill-rule="evenodd" stroke-width=".3" d="M62.425 91.414l95.605 30.923-2.832 8.757-95.605-30.922zM113.084 61.13l95.604 30.922-2.832 8.757-95.605-30.922zM62.425 154.79l95.605 30.922-2.833 8.758-95.604-30.923zM113.097 124.408l95.604 30.923-2.832 8.757-95.605-30.922z"/></symbol><symbol viewBox="0 0 24 24" id="settings" xmlns="http://www.w3.org/2000/svg"><path d="M12 15.5A3.5 3.5 0 0 1 8.5 12 3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97 0-.33-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1 0 .33.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.66z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="shaderlab" xmlns="http://www.w3.org/2000/svg"><path d="M9.11 17H6.5l-4.91-5L6.5 7h2.61l1.31-2.26L17.21 3l1.87 6.74L17.77 12l1.31 2.26L17.21 21l-6.79-1.74L9.11 17m.14-.25l5.13 1.38L11.42 13H5.5l3.75 3.75m6.87.38L17.5 12l-1.38-5.13L13.15 12l2.97 5.13M9.25 7.25L5.5 11h5.92l2.96-5.13-5.13 1.38z" fill="#1976d2"/></symbol><symbol viewBox="0 0 24 24" id="slim" xmlns="http://www.w3.org/2000/svg"><path d="M6.959 2.5a4.605 4.605 0 0 0-4.615 4.615v9.957a4.605 4.605 0 0 0 4.615 4.615h9.957a4.605 4.605 0 0 0 4.615-4.615V7.115A4.605 4.605 0 0 0 16.916 2.5zm4.938 2.691a6.811 6.811 0 0 1 6.81 6.813H13.43L9.938 7.287l.699 4.717H5.086a6.811 6.811 0 0 1 6.81-6.813z" fill="#f57f17"/></symbol><symbol id="smarty" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.iust0{fill:#ffce00}</style><path class="iust0" d="M9.14 20.606c0 .556.398.953.954.953h3.812c.556 0 .953-.397.953-.953v-.953H9.141zM12 2.5c-3.653 0-6.671 3.018-6.671 6.671 0 2.303 1.112 4.289 2.859 5.48v2.144c0 .556.397.953.953.953h5.718c.556 0 .953-.397.953-.953V14.65c1.747-1.191 2.86-3.177 2.86-5.48 0-3.653-3.019-6.671-6.672-6.671zm2.7 10.563l-.794.555v2.224h-3.812v-2.224l-.794-.555A4.712 4.712 0 0 1 7.235 9.17 4.78 4.78 0 0 1 12 4.405a4.78 4.78 0 0 1 4.765 4.765 4.712 4.712 0 0 1-2.065 3.892z"/></symbol><symbol viewBox="0 0 200 200" id="snyk" xmlns="http://www.w3.org/2000/svg"><title>Group 2</title><g transform="translate(15.255 18.22) scale(1.8477)" fill="none" fill-rule="evenodd"><path d="M65.161 24.997c-1.656 5.974-5.255 23.587-5.255 23.587s-6.618-2.464-14.148-2.476h-.055c-.413.002-.822.012-1.23.026v41.649h6.677v.003h5.815v-.003h20.858c.111-8.177-2.036-27.066-2.036-27.066-1.088-2.279.46-7.668.46-7.668-8.869-9.092-11.086-28.051-11.086-28.051zm-3.357 43.958c5.476 0 1.381 4.64.9 5.168H52.35c.944-1.18 4.504-5.168 9.453-5.168z" fill="#607d8b" stroke-width="1.6"/><path d="M26.366 24.995s-2.217 18.961-11.087 28.053c0 0 1.548 5.391.46 7.669 0 0-2.15 18.895-2.038 27.066h19.273v.003h7.079v-.003h5.744V46.107h-.025c-7.532.013-14.151 2.478-14.151 2.478s-3.6-17.615-5.255-23.59zm3.264 43.96c4.95 0 8.51 3.987 9.452 5.168H28.73c-.479-.528-4.573-5.168.9-5.168z" fill="#90a4ae" stroke-width="1.6"/><g transform="translate(23.76 77.45) scale(1.5998)"><g transform="translate(17.526)"><path d="M7.357.06H.177v.075C.177 2.64 2.345 4.67 4.89 4.67 7.431 4.67 9.6 2.64 9.6.135V.059z" fill="#455a64"/><path d="M1.972.06v.075a2.692 2.692 0 1 0 5.386 0V.059z" fill="#fff"/><path d="M5.496.06H4.234c-.012 0-.023.005-.034.007.157.033.243.388.21.624a.721.721 0 0 1-.71.617c.102.471.487.85.997.922a1.188 1.188 0 0 0 1.35-1.007C6.112.743 5.881.06 5.495.06z" fill="#37474f"/></g><path d="M7.552.06H.372v.075c0 2.505 2.17 4.535 4.712 4.535 2.544 0 4.712-2.03 4.712-4.535V.059z" fill="#455a64"/><path d="M2.168.06v.075a2.692 2.692 0 1 0 5.385 0V.059z" fill="#fff"/><path d="M5.692.06H4.428c-.01 0-.022.005-.032.007.156.033.242.388.21.624a.72.72 0 0 1-.712.617c.104.471.488.85.999.922A1.187 1.187 0 0 0 6.24 1.223C6.308.743 6.078.06 5.69.06z" fill="#37474f"/></g><path d="M25.514-.27l-4.202 7.697C19.838 10.17 6.858 34.465 6.858 43.243v.516L12.8 59.573c-.8 7.258-2.203 21.643-1.78 28.21h5.73c-.354-3.787.648-17.008 1.903-28.25l.076-.677-1.075-2.892c3.694-3.868 6.285-9.193 8.073-14.261l.174 1.235 5.869 9.629 2.291-.983c.058-.024 5.935-2.523 11.643-2.523 5.672 0 11.646 2.5 11.702 2.525l2.29.976 5.86-9.626.23-1.608c1.769 5.117 4.358 10.536 8.07 14.49l-1.127 3.035.076.678c1.259 11.286 2.266 24.564 1.916 28.252h5.677c.406-6.567-1.05-20.952-1.848-28.208l5.838-15.817v-.514c0-8.779-12.876-33.074-14.347-35.816L65.923-.27l-5.897 41.229-2.723 4.478c-2.628-.882-7.1-2.11-11.603-2.11-4.498 0-8.94 1.225-11.557 2.108l-2.722-4.476-2.07-14.452a.832.832 0 0 0 .006-.071l-.016-.004zm-3.166 18.39l1.206 8.407c-.46 3.143-2.561 15.47-8.198 23.24l-2.598-6.99c.325-4.554 5.067-15.462 9.59-24.656zm46.763 0c4.523 9.194 9.267 20.104 9.592 24.657L76.166 49.6c-6.09-8.553-8-22.459-8.166-23.73z" fill="#607d8b" stroke-width="1.6"/></g></symbol><symbol viewBox="0 0 24 24" id="solidity" xmlns="http://www.w3.org/2000/svg"><path d="M5.8 14.05l6.253 8.61 6.252-8.61-6.254 3.807z" fill="#0288d1" stroke-width="4.553" stroke-linejoin="round"/><path d="M12.051 1.347L5.8 11.833l6.252 3.807 6.254-3.807z" fill="#0288d1" stroke-width="5.025" stroke-linejoin="round"/></symbol><symbol viewBox="0 0 120 120" id="sonar" xmlns="http://www.w3.org/2000/svg"><style>.a,.b{fill:#fff}.b{stroke:#fff;stroke-miterlimit:10}</style><path d="M115.45 23.033S97.961 33.27 97.534 33.412c-.427.284-.852.57-1.137.854-1.422 1.421-1.848 3.41-1.422 5.26.285.852.711 1.849 1.422 2.56.711.71 1.564 1.137 2.559 1.422 1.848.426 3.84 0 5.262-1.422.426-.427.709-.853.851-1.28l.143-.427 2.56-4.692zm-39.102 9.242c-27.441 0-31.99 13.08-31.99 29.29 0 3.838.569 7.962-1.99 11.942-3.84 5.972-8.957 5.828-10.236 5.828-1.706 0-7.962-.993-8.246-2.841h.994c6.682 0 11.658-5.404 11.658-12.655v-2.56h-5.686c-4.123 0-7.82 1.849-10.238 5.12-2.417-3.271-6.113-5.12-10.236-5.12h-5.83v2.56c0 7.11 5.688 12.795 12.797 12.795h1.848c0 4.124 5.687 20.332 47.63 20.332 16.352 0 40.665-2.843 40.665-33.697 0-5.829-1.848-11.23-4.691-15.78-.996.284-1.992.568-3.13.568a8.92 8.92 0 0 1-8.956-8.957c0-.995.141-1.991.425-2.986-4.265-2.702-8.53-3.838-14.787-3.838z" fill="#1e88e5" stroke-width="1.422"/></symbol><symbol viewBox="0 0 412 395" id="stylelint" xmlns="http://www.w3.org/2000/svg"><title>stylelint-icon-white</title><g transform="translate(31.478 29.499) scale(.84775)" fill="#cfd8dc" fill-rule="evenodd"><path d="M208.8 393.05c45.057-161.12 43.75-161.85 76.32-276.73l7.832 4.523c4.255 2.458 7.738.448 7.738-4.455V61.602c8.643-30.27 15.416-53.66 17.4-60.693h35.287l58.618 54.304-38.498 33.27 29.11 31.473-191.86 273.09c-.938 1.542-2.244 1.19-1.947 0zm20.96-347.28c1.733 0 3.148.958 3.148 2.147v28.077c0 1.186-1.415 2.15-3.147 2.15h-47.396c-1.742 0-3.153-.96-3.153-2.15V47.917c0-1.185 1.41-2.147 3.153-2.147h47.396z"/><path d="M288.26 14.688l-52.14 30.1c.605.92.973 1.98.973 3.136v28.078c0 1.457-.565 2.77-1.496 3.83l52.663 30.402c3.59 2.073 6.535.377 6.535-3.764V18.456c0-4.145-2.944-5.836-6.535-3.768zM175.02 76V47.923c0-1.15.368-2.21.966-3.13l-52.14-30.105c-3.588-2.068-6.53-.376-6.53 3.768v88.013c0 4.14 2.938 5.84 6.53 3.76l52.66-30.405c-.926-1.06-1.487-2.37-1.487-3.827z"/><path d="M201.25 393.05h1.947c-45.05-161.12-43.753-161.85-76.32-276.73l-7.833 4.523c-4.253 2.458-7.737.448-7.737-4.455V61.602C102.662 31.332 95.892 7.942 93.902.909H58.619L.002 55.213l38.494 33.27-29.11 31.473z"/><circle cx="204.57" cy="122.54" r="14.231"/><circle cx="204.57" cy="207.16" r="14.231"/><circle cx="204.57" cy="291.78" r="14.23"/></g></symbol><symbol viewBox="0 0 412 395" id="stylelint_light" xmlns="http://www.w3.org/2000/svg"><title>stylelint-icon-black</title><g transform="translate(31.478 29.499) scale(.84775)" fill="#546e7a" fill-rule="evenodd"><path d="M208.8 393.05c45.057-161.12 43.75-161.85 76.32-276.73l7.832 4.523c4.255 2.458 7.738.448 7.738-4.455V61.602c8.643-30.27 15.416-53.66 17.4-60.693h35.287l58.618 54.304-38.498 33.27 29.11 31.473-191.86 273.09c-.938 1.542-2.244 1.19-1.947 0zm20.96-347.28c1.733 0 3.148.958 3.148 2.147v28.077c0 1.186-1.415 2.15-3.147 2.15h-47.396c-1.742 0-3.153-.96-3.153-2.15V47.917c0-1.185 1.41-2.147 3.153-2.147h47.396z"/><path d="M288.26 14.688l-52.14 30.1c.605.92.973 1.98.973 3.136v28.078c0 1.457-.565 2.77-1.496 3.83l52.663 30.402c3.59 2.073 6.535.377 6.535-3.764V18.456c0-4.145-2.944-5.836-6.535-3.768zM175.02 76V47.923c0-1.15.368-2.21.966-3.13l-52.14-30.105c-3.588-2.068-6.53-.376-6.53 3.768v88.013c0 4.14 2.938 5.84 6.53 3.76l52.66-30.405c-.926-1.06-1.487-2.37-1.487-3.827z"/><path d="M201.25 393.05h1.947c-45.05-161.12-43.753-161.85-76.32-276.73l-7.833 4.523c-4.253 2.458-7.737.448-7.737-4.455V61.602C102.662 31.332 95.892 7.942 93.902.909H58.619L.002 55.213l38.494 33.27-29.11 31.473z"/><circle cx="204.57" cy="122.54" r="14.231"/><circle cx="204.57" cy="207.16" r="14.231"/><circle cx="204.57" cy="291.78" r="14.23"/></g></symbol><symbol viewBox="0 0 200.00001 200.00001" id="stylus" xmlns="http://www.w3.org/2000/svg"><path d="M126.814 155.9c14.64-17.51 16.362-35.595 5.024-69.18-7.177-21.24-19.09-37.602-10.334-50.807 9.329-14.065 29.135-.43 12.63 18.371l3.301 2.297c19.806 2.296 29.566-24.83 14.783-32.58C113.179 3.621 79.02 42.803 94.09 88.156c6.458 19.232 15.5 39.613 8.18 55.83-6.314 13.923-18.514 22.103-26.695 22.39-17.079.862-5.74-38.32 13.922-48.08 1.722-.861 4.162-2.01 1.866-4.88-24.256-2.727-38.464 8.468-46.645 24.112-23.825 45.497 45.21 62.29 82.095 18.371z" fill="#c0ca33" stroke-width="1.435"/></symbol><symbol viewBox="0 0 24 24" id="swc" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="jba"><stop offset="0" stop-color="#791223"/><stop offset="1" stop-color="#d92f3c"/></linearGradient><linearGradient xlink:href="#jba" id="jbb" x1="12.356" y1="21.559" x2="12.356" y2="2.949" gradientUnits="userSpaceOnUse"/></defs><path d="M6 3c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6 3 6.5V19a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6.5c0-.5-.17-.93-.46-1.27l-1.39-1.68C18.88 3.21 18.47 3 18 3H6zm-.07 1h12l.94 1H5.12l.81-1z" fill="url(#jbb)"/><path style="line-height:125%" d="M11.053 11.918h-.008c-.244.022-.475.054-.676.11a2.9 2.9 0 0 0-.856.412 3.399 3.399 0 0 0-.67.683 9.36 9.36 0 0 0-.586.95c-.07.131-.134.244-.201.365v.001h-.002l-.768 1.372-.003-.001c-.136.253-.264.485-.38.686-.123.212-.26.39-.411.539a1.599 1.599 0 0 1-.52.34c-.04.016-.092.024-.138.036h-.567v1.383H5.834v-.001c.245-.02.477-.053.679-.11a2.9 2.9 0 0 0 .856-.411c.245-.185.469-.413.67-.683.195-.275.39-.591.585-.95.07-.131.135-.244.202-.366l.004.001.002-.002.02-.038H10.948v-1.378h-.19v-.001H9.624c.125-.234.246-.452.355-.64.123-.21.259-.39.41-.538.152-.148.325-.26.52-.34.04-.015.091-.024.136-.035h.57V13.3h-.002v-1.381h-.56v-.001z" font-weight="400" font-size="40" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#fff"/></symbol><symbol viewBox="0 0 24 24" id="swift" xmlns="http://www.w3.org/2000/svg"><path d="M17.09 19.72c-2.36 1.36-5.59 1.5-8.86.1A13.807 13.807 0 0 1 2 14.5c.67.55 1.46 1 2.3 1.4 3.37 1.57 6.73 1.46 9.1 0-3.37-2.59-6.24-5.96-8.37-8.71-.45-.45-.78-1.01-1.12-1.51 8.28 6.05 7.92 7.59 2.41-1.01 4.89 4.94 9.43 7.74 9.43 7.74.16.09.25.16.36.22.1-.25.19-.51.26-.78.79-2.85-.11-6.12-2.08-8.81 4.55 2.75 7.25 7.91 6.12 12.24-.03.11-.06.22-.05.39 2.24 2.83 1.64 5.78 1.35 5.22-1.21-2.39-3.48-1.65-4.62-1.17z" fill="#fe5e2f"/></symbol><symbol viewBox="0 0 24 24" id="table" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m7 1.5V9h5.5L13 3.5m4 7.5h-4v2h1l-2 1.67L10 13h1v-2H7v2h1l3 2.5L8 18H7v2h4v-2h-1l2-1.67L14 18h-1v2h4v-2h-1l-3-2.5 3-2.5h1v-2z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 200 200" id="terraform" xmlns="http://www.w3.org/2000/svg"><g transform="translate(177.03 -58.705) scale(.92881)" fill="#5c6bc0" stroke="#b0aff5" stroke-linejoin="round"><g stroke-width=".288"><path transform="skewY(26.439) scale(.89541 1)" d="M-203.8 170.95h64.714v51.88H-203.8zM-124.37 171.04h64.714v51.88h-64.714zM-124.37 236.09h64.714v51.88h-64.714z"/></g><path transform="skewY(-22.59) scale(-.92328 1)" stroke-width=".284" d="M-19.172 128.27h62.76v51.88h-62.76z"/></g></symbol><symbol viewBox="0 0 24 24" id="test-js" xmlns="http://www.w3.org/2000/svg"><path d="M5 19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57L13 8.35V4h-2v4.35L5.18 18.43c-.11.16-.18.36-.18.57m1 3a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3H6m7-6l1.34-1.34L16.27 18H7.73l2.66-4.61L13 16m-.5-4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="test-jsx" xmlns="http://www.w3.org/2000/svg"><path d="M5 19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57L13 8.35V4h-2v4.35L5.18 18.43c-.11.16-.18.36-.18.57m1 3a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3H6m7-6l1.34-1.34L16.27 18H7.73l2.66-4.61L13 16m-.5-4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#00bcd4"/></symbol><symbol viewBox="0 0 24 24" id="test-ts" xmlns="http://www.w3.org/2000/svg"><path d="M5 19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57L13 8.35V4h-2v4.35L5.18 18.43c-.11.16-.18.36-.18.57m1 3a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3H6m7-6l1.34-1.34L16.27 18H7.73l2.66-4.61L13 16m-.5-4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#0288d1"/></symbol><symbol viewBox="0 0 500 500" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="tex" xmlns="http://www.w3.org/2000/svg"><g font-weight="400" font-size="40" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#42a5f5" stroke-linejoin="miter"><text style="line-height:125%" x="9.914" y="364.919"><tspan x="9.914" y="364.919" font-size="287.5">T</tspan></text><text style="line-height:125%" x="136.374" y="435.558"><tspan x="136.374" y="435.558" font-size="287.5">E</tspan></text><text style="line-height:125%" x="307.819" y="361.201"><tspan x="307.819" y="361.201" font-size="287.5">X</tspan></text></g></symbol><symbol viewBox="0 0 24 24" id="todo" xmlns="http://www.w3.org/2000/svg"><path d="M3 5h6v6H3V5m2 2v2h2V7H5m6 0h10v2H11V7m0 8h10v2H11v-2m-6 5l-3.5-3.5 1.41-1.41L5 17.17l4.59-4.58L11 14l-6 6z" fill="#42a5f5"/></symbol><symbol id="travis" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><style id="jkstyle2">.jkst0{fill:#cb3349}.jkst1{fill:#f4edae}.jkst2{fill:#e6ccad}.jkst3{fill:#656c67}.jkst4{fill:#e5caa3}.jkst5{fill:#c7b39a}.jkst6{fill:#ebd599}.jkst7{fill:#2d3136}.jkst8{fill:#edf6fa}.jkst9{opacity:.8}.jkst10{opacity:.75;fill:#ebd599}</style><g id="jkg99" transform="translate(11.017 12.484) scale(.8858)"><g id="jkg10"><path class="jkst0" d="M47.781 86.572s-31.118 21.903-32.335 30.247l2.335-.48S55.045 91.64 84.584 88.628l.669-3.749z" id="jkpath4" fill="#cb3349"/><path class="jkst0" d="M96.629 83.442l-24.511 17.385 1.325 1.063c.999-.806 43.539-13.798 43.539-13.798l8.969-5.623c-6.018.749-29.322.973-29.322.973z" id="jkpath6" fill="#cb3349"/><path class="jkst0" d="M117.932 104.469c17.405 0 43.495-17.046 43.495-17.046l-8.434-1.605c-.417.417-13.6-.462-13.6-.462l-6.258-1.738-14.951 17.036-1.217 2.956c1.075-.437.965.859.965.859z" id="jkpath8" fill="#cb3349"/></g><path class="jkst0" d="M174.728 158.832l-5.377 1.514-24.843-.537-15.541-12.085-18.784 4.7-21.726-1.88-12.166 13.294-22.828 6.819-11.398-3.534-.574-.494 5.116 12.527s11.588 12.424 18.061 13.885c6.472 1.461 18.165-.105 26.935-1.463 8.769-1.357 15.764-4.489 18.582-9.603 2.818-5.117 3.236-6.578 3.236-6.578s8.353 11.797 15.556 13.155c7.203 1.357 28.605-5.952 28.605-5.952s13.051-3.549 15.346-8.038c2.297-4.489 8.353-19.209 8.353-19.209zM44.456 169.038l-.361-.166-2.013-1.736z" id="jkpath12" fill="#cb3349"/><g id="jkg97"><path class="jkst1" d="M195.832 70.085a48.125 48.125 0 0 0-.21-2.009 26.472 26.472 0 0 0-.215-1.424c-1.793-1.509-3.831-2.851-5.952-4.071-2.299-1.343-4.704-2.546-7.159-3.663-2.438-1.15-4.942-2.191-7.461-3.207a134.313 134.313 0 0 0-3.798-1.477c-1.269-.495-2.55-.956-3.835-1.424 2.697.447 5.366 1.059 8.015 1.741 1.723.446 3.437.945 5.14 1.477-12.112-31.655-41.07-52.27-72.687-52.27-31.622 0-60.577 20.615-72.686 52.27a109.044 109.044 0 0 1 5.137-1.477c2.653-.682 5.323-1.294 8.018-1.741-1.289.468-2.567.929-3.84 1.424-1.267.472-2.536.967-3.798 1.477-2.519 1.016-5.016 2.057-7.46 3.207-2.45 1.117-4.857 2.32-7.156 3.663-2.121 1.219-4.157 2.562-5.957 4.071-.075.457-.151.951-.21 1.424a51.768 51.768 0 0 0-.21 2.009 51.354 51.354 0 0 0-.177 4.061 59.216 59.216 0 0 0 .5 8.11c.37 2.692.864 5.366 1.595 7.951.36 1.295.768 2.572 1.24 3.808.237.617.495 1.225.764 1.816.134.294.274.585.413.864l.172.328c.199.101.408.204.607.3l1.204.575c.671.305 1.6.746 2.368 1.09.043-.037.086-.075.123-.114l-2.235-8.513c.474-.13 4.718-1.225 12.032-2.617a38.816 38.816 0 0 1-1.772-.381c-1.665-.414-3.309-.919-4.899-1.564a22.415 22.415 0 0 1-2.309-1.115c-.742-.426-1.472-.908-2.037-1.548 8.036 2.622 24.64 1.434 39.399-.091 13.499-1.391 27.029-2.293 40.63-2.32 13.602.027 27.137.929 40.63 2.32 14.766 1.525 31.37 2.713 39.405.091-.564.64-1.293 1.123-2.035 1.548a22.5 22.5 0 0 1-2.308 1.115c-1.592.645-3.234 1.15-4.899 1.564-.247.059-.496.113-.743.166 8.02 1.488 12.689 2.697 13.188 2.831l-2.138 8.11c.43-.194.864-.381 1.29-.574l1.202-.575c.2-.097.403-.199.607-.3l.166-.328c.146-.279.286-.57.419-.864.27-.591.528-1.199.764-1.816a42.235 42.235 0 0 0 1.241-3.808c.731-2.585 1.225-5.259 1.595-7.951.345-2.685.526-5.398.501-8.11a50.874 50.874 0 0 0-.179-4.059z" id="jkpath14" fill="#f4edae"/><path class="jkst2" d="M116.787 182.661c-1.064.16-2.128.295-3.186.375-.682.033-1.404.102-2.059.102l-.242.005c.822-1.837 1.446-3.26 1.919-4.339.963 1.08 2.188 2.417 3.568 3.857z" id="jkpath16" fill="#e6ccad"/><path class="jkst2" d="M119.101 185.018c3.304 3.272 7.398 5.146 11.904 5.479-7.569 3.074-14.702 4.26-20.197 4.63-5.478.367-11.032-.279-16.474-1.771.456-.082.79-.14 1.193-.189.447-.054 10.206-1.327 14.605-7.868l.413.009 1.08-.009c.731 0 1.395-.06 2.094-.087a43.69 43.69 0 0 0 4.878-.703c.167.171.333.338.504.509z" id="jkpath18" fill="#e6ccad"/><path class="jkst3" d="M128.464 87.071a98.82 98.82 0 0 1-1.048 1.343c-1.933 2.444-4.614 5.57-7.794 8.627a369.585 369.585 0 0 0-11.404-.177c-6.46 0-12.655.171-18.537.457 8.311-3.449 18.296-6.818 29.109-8.842a113.323 113.323 0 0 1 9.674-1.408z" id="jkpath20" fill="#656c67"/><path class="jkst3" d="M79.821 90.792c-2.966 2.084-6.317 4.744-9.566 7.971a360.155 360.155 0 0 0-21.567 2.81c9.207-4.232 19.713-8.127 31.133-10.781z" id="jkpath22" fill="#656c67"/><path class="jkst3" d="M181.48 107.969l-3.384 23.679-16.212 11.355-42.283-4.807-6.365-20.961a1.383 1.383 0 0 0-1.108-.971c-1.567-.253-2.953-.382-4.108-.382-1.16 0-2.541.129-4.115.382-.522.086-.95.461-1.106.971l-6.209 20.45-42.047 9.357-16.662-11.672-3.283-26.572c.715-.404 1.441-.806 2.176-1.209 1.031-.222 2.191-.457 3.475-.704l3.094 25.073c.048.392.264.741.586.967l11.462 8.032a1.425 1.425 0 0 0 1.101.213l34.57-7.692c.119-.027.237-.069.344-.124a1.39 1.39 0 0 0 .682-.827l6.225-20.498c1.67-.43 5.947-1.429 9.706-1.429 3.749 0 8.03.999 9.701 1.429l6.225 20.498c.161.532.624.912 1.176.977l34.57 3.927c.335.037.677-.05.952-.242l11.469-8.025c.31-.22.52-.566.573-.946l3.062-21.421c2.301.444 4.224.846 5.733 1.172z" id="jkpath24" fill="#656c67"/><path class="jkst3" d="M185.751 93.119l-2.976 11.29c-6.086-1.342-19.456-3.975-37.654-5.747 5.946-2.535 12-5.715 17.531-9.69 10.829 1.53 18.78 3.169 23.099 4.147z" id="jkpath26" fill="#656c67"/><g id="jkg32"><path class="jkst4" d="M63.841 128.441c2.357-1.274 5.021-1.085 9.19-1.079.447.011.908.005 1.39-.005.41-.005.822-.011 1.258-.022 4.296-.042 7.869.366 7.806-6.381-.065-6.746-3.062-12.198-7.354-12.155-4.297.037-8.454 5.564-8.197 12.306.07 1.756.328 3.023.742 3.937-3.745.938-4.777 3.254-4.835 3.399zm51.657-27.749a46.634 46.634 0 0 1-5.249 3.712l-6.097 3.68a52.065 52.065 0 0 0-7.331 1.467 1.216 1.216 0 0 0-.317.14 1.406 1.406 0 0 0-.629.794l-6.209 20.46-33.185 7.38-10.452-7.321-3.041-24.634c5.936-1.09 13.874-2.352 23.41-3.42a56.802 56.802 0 0 0-2.955 3.855l-5.677 8.149 8.266-5.511c.123-.086 5.387-3.549 13.998-7.761a377.407 377.407 0 0 1 35.468-.99z" id="jkpath28" fill="#e5caa3"/><path class="jkst4" d="M151.835 125.675c-.042-.16-.945-2.873-4.942-2.397.461-1.003.666-2.356.521-4.21-.528-6.731-4.443-12.08-8.735-11.931-4.292.152-7.042 5.731-6.805 12.478.236 6.741 3.84 6.694 8.132 6.543 5.77-.107 8.939-1.88 11.829-.483zm21.18-19.385l-2.992 20.944-10.539 7.379-33.141-3.766-6.183-20.363a1.41 1.41 0 0 0-.945-.934c-.205-.06-4.308-1.23-8.659-1.607l.795-.053c.687-.049 12.118-1.451 25.767-6.157 15.115 1.161 27.458 3.02 35.897 4.557z" id="jkpath30" fill="#e5caa3"/></g><g id="jkg38"><path class="jkst5" d="M63.841 128.441c2.357-1.274 5.021-1.085 9.19-1.079.447.011.908.005 1.39-.005.41-.005.822-.011 1.258-.022 4.296-.042 7.869.366 7.806-6.381-.065-6.746-3.062-12.198-7.354-12.155-4.297.037-8.454 5.564-8.197 12.306.07 1.756.328 3.023.742 3.937-3.745.938-4.777 3.254-4.835 3.399zm51.657-27.749a46.634 46.634 0 0 1-5.249 3.712l-6.097 3.68a52.065 52.065 0 0 0-7.331 1.467 1.216 1.216 0 0 0-.317.14 1.406 1.406 0 0 0-.629.794l-6.209 20.46-33.185 7.38-10.452-7.321-3.041-24.634c5.936-1.09 13.874-2.352 23.41-3.42a56.802 56.802 0 0 0-2.955 3.855l-5.677 8.149 8.266-5.511c.123-.086 5.387-3.549 13.998-7.761a377.407 377.407 0 0 1 35.468-.99z" id="jkpath34" fill="#c7b39a"/><path class="jkst5" d="M151.835 125.675c-.042-.16-.945-2.873-4.942-2.397.461-1.003.666-2.356.521-4.21-.528-6.731-4.443-12.08-8.735-11.931-4.292.152-7.042 5.731-6.805 12.478.236 6.741 3.84 6.694 8.132 6.543 5.77-.107 8.939-1.88 11.829-.483zm21.18-19.385l-2.992 20.944-10.539 7.379-33.141-3.766-6.183-20.363a1.41 1.41 0 0 0-.945-.934c-.205-.06-4.308-1.23-8.659-1.607l.795-.053c.687-.049 12.118-1.451 25.767-6.157 15.115 1.161 27.458 3.02 35.897 4.557z" id="jkpath36" fill="#c7b39a"/></g><path class="jkst2" d="M187.481 115.502c.508.419.911 1.504.456 6.558-.559 6.188-3.16 17.049-4.771 18.8-1.778.344-5.505-.064-7.778-.595.393-1.559.505-2.306.822-3.9l3.975-2.781c.317-.22.526-.566.58-.941l2.778-19.466c1.686.912 3.421 1.899 3.938 2.325z" id="jkpath40" fill="#e6ccad"/><path class="jkst2" d="M40.937 140.908c.199.704.408 1.407.624 2.1-2.139.628-6.495 1.23-8.465.886-1.633-1.645-4.679-12.966-5.345-18.978-.543-4.871-.162-5.924.333-6.334.575-.483 2.728-1.708 4.593-2.707l2.519 20.449c.048.393.257.741.586.967z" id="jkpath42" fill="#e6ccad"/><path class="jkst2" d="M121.347 141.194l-.151 1.305s-4.581 4.248-11.956 5.199c-7.375.95-13.171-3.582-13.171-3.582.242.788.586 2.567 2.256 4.086a53.184 53.184 0 0 0-6.313-.393c-.804 0-1.616.023-2.401.061-4.539.237-10.924 7.1-15.414 14.014-2.203.697-9.089 2.883-17.06 5.237-7.44-10.309-11.098-20.842-11.469-21.932l.005-.006c-.15-.419-.301-.839-.441-1.268l1.913 1.338v.005l4.726 3.309 1.58 1.101c.236.167.515.253.794.253.102 0 .204-.011.305-.031l43.435-9.67a1.385 1.385 0 0 0 1.025-.95l6.194-20.39c1.069-.145 2.008-.22 2.814-.22.801 0 1.746.075 2.815.22l6.374 20.997c.162.532.624.919 1.171.977z" id="jkpath44" fill="#e6ccad"/><path class="jkst2" d="M170.926 140.066l1.402-.984c-.232.973-.484 1.94-.747 2.896-1.949 6.248-4.25 11.774-6.805 16.656-.565.039-1.161.061-1.8.061-1.972 0-3.986-.167-6.215-.371-3.868-.355-10.007-1.058-11.946-1.283-1.67-1.332-7.385-5.873-12.14-9.615-.187-.151-.348-.291-.505-.42-.837-.708-1.789-1.513-3.717-1.513-1.751 0-4.308.638-10.489 2.508 3.212-2.401 3.233-5.5 3.233-5.5l.151-1.305 40.748 4.629a1.41 1.41 0 0 0 .955-.241l4.094-2.868z" id="jkpath46" fill="#e6ccad"/><path class="jkst6" d="M140.937 54.337c.124 3.625.033 10.194-1.655 16.345a1.335 1.335 0 0 0 0 .704 259.298 259.298 0 0 0-6.446-.591c2.412-5.054 2.938-10.436 3.052-12.332 1.852-1.317 3.696-2.896 5.049-4.126z" id="jkpath48" fill="#ebd599"/><path class="jkst6" d="M79.456 58.462c.112 1.896.638 7.267 3.046 12.317-2.149.171-4.297.37-6.441.596a1.328 1.328 0 0 0 0-.694c-1.686-6.139-1.772-12.714-1.654-16.345 1.353 1.231 3.19 2.81 5.049 4.126z" id="jkpath50" fill="#ebd599"/><path class="jkst7" d="M151.835 125.675c-2.89-1.396-6.059.377-11.828.484-4.292.151-7.896.198-8.132-6.543-.237-6.747 2.513-12.326 6.805-12.478 4.292-.15 8.207 5.2 8.735 11.931.145 1.854-.06 3.207-.521 4.21 3.996-.477 4.899 2.235 4.941 2.396zm-13.488-9.878a2.203 2.203 0 0 0 2.154-2.235 2.186 2.186 0 0 0-2.235-2.153 2.194 2.194 0 0 0 .081 4.388z" id="jkpath52" fill="#2d3136"/><circle transform="rotate(-1.049 138.093 113.428)" class="jkst8" cx="138.307" cy="113.602" id="jkellipse54" r="2.194" fill="#edf6fa"/><path class="jkst7" d="M83.484 120.953c.063 6.747-3.509 6.339-7.806 6.381-.435.011-.848.016-1.258.022-.482.011-.944.016-1.39.005-4.168-.005-6.833-.194-9.19 1.079.058-.145 1.09-2.461 4.835-3.4-.414-.914-.673-2.181-.742-3.937-.257-6.741 3.9-12.269 8.197-12.306 4.292-.042 7.289 5.411 7.354 12.156zm-6.634-3.529a2.195 2.195 0 1 0-.122-4.388 2.195 2.195 0 0 0 .122 4.388z" id="jkpath56" fill="#2d3136"/><circle transform="rotate(-1.473 76.78 115.216)" class="jkst8" cx="76.79" cy="115.23" id="jkellipse58" r="2.195" fill="#edf6fa"/><g class="jkst9" id="jkg64" opacity=".8"><path class="jkst6" d="M50.691 75.155s.667-8.692 2.03-12.023c.702-1.717 4.996-2.81 8.276-3.591 3.278-.78 8.508-2.342 9.524 2.264 1.015 4.606 2.653 7.963 3.746 9.446l-1.404-18.97-22.562 5.464-1.484 16.786.703 1.327 1.171-.703" id="jkpath60" fill="#ebd599"/><path class="jkst6" d="M164.855 75.155s-.666-8.692-2.029-12.023c-.703-1.717-4.997-2.81-8.275-3.591-3.28-.78-8.51-2.342-9.526 2.264-1.013 4.606-2.654 7.963-3.748 9.446l1.407-18.97 22.562 5.464 1.483 16.786-.703 1.327-1.171-.703" id="jkpath62" fill="#ebd599"/></g><path class="jkst10" d="M132.965 18.378s-.598 45.49-11.224 45.49h-14.875-12.752c-10.626 0-11.484-45.47-11.484-45.47l-5.22 15.438.085 21.183 3.707 2.947 1.685 9.096 2.357 5.307 45.482.084 2.105-3.791 1.769-6.4.254-4.043 5.023-14.341z" id="jkpath66" opacity=".75" fill="#ebd599"/><path class="jkst10" d="M166.429 60.794s2.187 15.692 7.974 18.522c5.788 2.829 0 0 0 0l-8.103-2.444z" id="jkpath68" opacity=".75" fill="#ebd599"/><path class="jkst10" d="M48.908 60.794s-2.187 15.692-7.975 18.522c-5.788 2.829 0 0 0 0l8.104-2.444z" id="jkpath70" opacity=".75" fill="#ebd599"/><path class="jkst7" d="M167.987 76.8c2.755.902 5.526 1.858 8.036 3.325-1.343-.532-2.729-.913-4.126-1.257a70.385 70.385 0 0 0-4.201-.924c-2.82-.531-5.65-.982-8.498-1.327-2.841-.37-5.687-.682-8.546-.924-2.858-.241-5.709-.483-8.573-.65-11.446-.704-22.924-.88-34.41-.892-11.483.006-22.962.221-34.409.897-2.862.166-5.715.409-8.572.651-2.857.241-5.71.548-8.546.923-2.847.345-5.678.796-8.498 1.327-1.407.264-2.81.57-4.206.919-1.391.344-2.783.725-4.126 1.257 2.509-1.466 5.28-2.427 8.041-3.331.232-.075.467-.139.703-.214-.015-.059-.032-.113-.043-.177-.048-.317-1.069-7.859.709-18.645.086-.516.456-.935.962-1.075l2.917-.831c.634-22.625 9.952-33.266 10.243-33.594-8.326 13.397-8.25 29.286-8.106 32.986l18.128-5.152c.016-.005.026-.005.042-.01.076-.016.151-.027.226-.032.021 0 .049-.006.075-.006a1.19 1.19 0 0 1 .297.027c.015 0 .031.011.053.016.075.016.145.042.224.075.033.016.054.033.086.049.058.033.119.07.177.112.016.011.034.016.049.033l.032.032c.016.016.037.027.054.044.012.016.494.493 1.262 1.209-.182-5.973.102-23.108 8.262-37.31-.172.498-6.646 19.428-4.415 40.645.724.58 1.486 1.149 2.229 1.649.359.247.58.655.585 1.09.006.07.161 6.833 3.148 12.586.042.086.074.177.102.268 7.429-.505 14.878-.709 22.312-.714 7.436.005 14.88.22 22.307.731.027-.097.06-.193.109-.285 2.986-5.753 3.142-12.516 3.142-12.586.01-.436.231-.843.591-1.09.741-.5 1.493-1.069 2.224-1.649 2.234-21.217-4.24-40.147-4.411-40.645 8.153 14.201 8.444 31.336 8.262 37.31a62.536 62.536 0 0 0 1.261-1.209c.016-.016.039-.027.053-.044.012-.01.018-.021.033-.032.016-.016.033-.022.049-.033.06-.042.119-.079.177-.118.028-.01.054-.027.081-.043.081-.033.155-.059.236-.08.016 0 .033-.011.049-.011.096-.021.2-.032.296-.027.027 0 .049.006.07.006.075.005.156.016.231.032.012.006.028.006.042.01l18.129 5.152c.146-3.7.221-19.59-8.104-32.986.289.328 9.609 10.969 10.237 33.594l2.922.831c.499.14.875.559.962 1.075 1.777 10.786.752 18.328.708 18.645-.01.065-.026.124-.042.182.239.07.47.139.707.215zm-3.297-.968c.14-1.207.789-7.809-.591-16.801l-20.52-5.833c.184 3.475.265 11.012-1.707 18.199a1.619 1.619 0 0 1-.101.258c.203.021.408.037.606.064 5.769.661 11.511 1.584 17.189 2.83 1.712.398 3.426.823 5.124 1.283zm-25.409-5.151c1.688-6.15 1.779-12.72 1.655-16.345-1.353 1.23-3.197 2.809-5.049 4.125-.114 1.896-.64 7.278-3.052 12.332 2.149.173 4.298.366 6.446.591a1.33 1.33 0 0 1 0-.703zm-56.78.098c-2.408-5.05-2.934-10.422-3.046-12.317-1.858-1.316-3.696-2.895-5.049-4.125-.119 3.631-.032 10.206 1.654 16.345.065.237.058.473 0 .694 2.145-.227 4.292-.425 6.441-.597zm-8.933.864a1.65 1.65 0 0 1-.098-.247c-1.975-7.187-1.889-14.723-1.712-18.199L51.244 59.03c-1.38 8.982-.736 15.583-.597 16.797 1.703-.462 3.411-.887 5.131-1.284 2.835-.628 5.693-1.154 8.556-1.638 2.869-.478 5.747-.843 8.626-1.192.205-.027.404-.042.608-.07z" id="jkpath72" fill="#2d3136"/><g id="jkXMLID_1_"><g id="jkg78"><path class="jkst7" d="M129.293 18.973v17.025h-12.068v-4.974h-2.72v22.981h4.109v12.85H97.505v-12.85h4.092v-22.98h-2.711v4.974h-12.06V18.973zm-3.626 13.408v-9.789H90.443v9.789h4.816v-4.974h9.964v30.225h-4.1v5.606h13.865v-5.606h-4.1V27.407h9.964v4.974z" id="jkpath74" fill="#2d3136"/><path class="jkst0" id="jkpolygon76" fill="#cb3349" d="M101.123 57.632h4.1V27.407h-9.964v4.974h-4.816v-9.79h35.224v9.79h-4.816v-4.974h-9.964v30.225h4.1v5.606h-13.864z"/></g></g><path class="jkst3" d="M30.694 93.119c1.759-.399 4.136-.907 7.051-1.47a104.37 104.37 0 0 0-6.222 4.597z" id="jkpath83" fill="#656c67"/><path class="jkst5" d="M95.111 139.78s.492 3.165-3.938 4.519c-4.428 1.355-32.482 9.716-35.682 9.263-3.199-.451-11.319-5.874-11.319-5.874l-1.969-7.004 12.016 7.492z" id="jkpath85" fill="#c7b39a"/><path class="jkst5" d="M120.242 139.167s-.354 3.182 4.131 4.345c4.484 1.161 32.875 8.295 36.05 7.704 3.176-.591 11.053-6.361 11.053-6.361l1.663-7.084-11.045 6.588z" id="jkpath87" fill="#c7b39a"/><path class="jkst5" d="M28.412 133.956s3.887 7.775 10.166 5.083l4.485 1.645-.448 3.29-9.419 1.195-2.541-1.494z" id="jkpath89" fill="#c7b39a"/><path class="jkst5" d="M187.551 131.822s-6.353 8.115-12.632 5.424l-2.019 1.302.448 3.289 9.419 1.196 2.54-1.495z" id="jkpath91" fill="#c7b39a"/><path class="jkst5" d="M89.279 192.904s23.03 11.611 49.106-4.188l-8.374-.571s-18.272 7.232-32.738 3.235z" id="jkpath93" fill="#c7b39a"/><path class="jkst7" d="M112.626 171.509l1.594 1.899c.036.046 3.577 4.26 7.906 8.552 2.879 2.853 6.357 4.297 10.343 4.297 1.361 0 2.791-.175 4.235-.523 1.34-.326 2.796-.673 4.287-1.03 5.384-1.287 11.482-2.749 14.438-3.577.585-.166 1.238-.315 1.925-.472 3.935-.909 9.329-2.163 12.187-7.889 2.149-4.297 5.047-9.874 7.197-13.961-1.863.859-3.816 1.79-5.203 2.52-2.138 1.123-4.938 1.667-8.558 1.667-2.152 0-4.266-.181-6.605-.389-4.675-.43-12.586-1.361-12.667-1.372l-.606-.067-.478-.383c-.071-.052-7.003-5.575-12.606-9.981-.227-.186-.434-.358-.621-.513-.59-.503-.59-.503-.942-.503-1.797 0-7.02 1.62-18.462 5.167l-.703.223-.689-.26c-.078-.026-7.585-2.81-16.581-2.81-.736 0-1.47.019-2.185.056-.901.046-5.958 2.448-12.425 12.68l-.419.657-.741.238c-.107.037-11.238 3.63-23.042 7.005l-.766.218-.725-.337c-.077-.031-4.696-2.174-9.091-4.194 2.397 3.541 5.462 7.958 8.159 11.422 4.711 6.067 10.649 11.674 22.034 11.674 1.428 0 2.945-.088 4.503-.265 11.581-1.309 14.563-1.837 16.168-2.117.543-.092.973-.171 1.522-.238.088-.011 9.571-1.237 12.232-7.206 2.744-6.134 3.298-7.595 3.319-7.651l.968-2.583s.12-.669.317-.877c0 .005 0 .005.005.005l.019.016c.305.219.757.902.757.902zM40.499 55.71c-2.516 1.014-5.016 2.06-7.46 3.209-2.449 1.119-4.856 2.32-7.155 3.66-2.121 1.222-4.157 2.563-5.954 4.076-.077.455-.149.952-.211 1.423a51.357 51.357 0 0 0-.388 6.068c-.026 2.713.16 5.426.502 8.112.372 2.692.864 5.369 1.594 7.952a41.963 41.963 0 0 0 1.243 3.804c.233.623.492 1.228.762 1.818.134.294.274.585.413.864l.172.326c.201.104.409.207.605.3l1.206.574c.673.311 1.6.751 2.366 1.093.046-.037.088-.078.124-.114l-2.231-8.511c.471-.129 4.717-1.227 12.032-2.619a33.744 33.744 0 0 1-1.775-.379 36.704 36.704 0 0 1-4.898-1.563 22.857 22.857 0 0 1-2.309-1.119c-.741-.425-1.471-.905-2.035-1.547 8.035 2.624 24.637 1.433 39.398-.088 13.501-1.393 27.028-2.293 40.628-2.325 13.6.031 27.138.931 40.63 2.325 14.77 1.522 31.374 2.713 39.406.088-.564.642-1.293 1.122-2.034 1.547-.739.42-1.522.782-2.309 1.119a36.965 36.965 0 0 1-4.903 1.563c-.244.056-.492.114-.741.166 8.02 1.486 12.689 2.697 13.186 2.832l-2.138 8.107c.43-.192.864-.377 1.288-.574l1.207-.574c.196-.094.404-.196.606-.3l.166-.326c.144-.279.284-.57.419-.864.27-.591.528-1.196.767-1.818.471-1.231.879-2.51 1.236-3.804.731-2.583 1.228-5.26 1.595-7.952.346-2.686.528-5.4.502-8.112a52.755 52.755 0 0 0-.176-4.059 51.573 51.573 0 0 0-.213-2.009 29.83 29.83 0 0 0-.213-1.423c-1.797-1.513-3.831-2.853-5.954-4.076-2.299-1.34-4.704-2.541-7.159-3.66-2.438-1.149-4.943-2.195-7.46-3.209a140.105 140.105 0 0 0-3.801-1.476c-1.267-.491-2.552-.956-3.835-1.423 2.696.445 5.369 1.06 8.013 1.739 1.724.446 3.444.948 5.141 1.481-12.11-31.658-41.07-52.272-72.685-52.272-31.622 0-60.576 20.614-72.684 52.272a107.832 107.832 0 0 1 5.135-1.481c2.651-.678 5.322-1.294 8.02-1.739-1.29.466-2.568.931-3.842 1.423-1.268.47-2.535.967-3.799 1.475zm159.43 18.316a53.972 53.972 0 0 1-.258 8.733 55.462 55.462 0 0 1-1.619 8.605c-.4 1.414-.86 2.811-1.404 4.198a38.295 38.295 0 0 1-.89 2.071c-.161.341-.331.678-.523 1.025l-.284.512a8.975 8.975 0 0 1-.348.574l-.294.457-.461.237c-.492.254-.895.445-1.342.653l-1.298.585a88.22 88.22 0 0 1-2.62 1.065c-.611.239-1.15.457-1.662.674l-1.444 5.487c-.036-.009-.471-.12-1.283-.315l-.078.574c1.594.833 4.726 2.522 5.793 3.403 2.148 1.775 2.299 4.587 1.823 9.841-.244 2.697-1.139 7.946-2.381 12.767-2.144 8.298-3.283 9.273-4.753 9.649-.746.192-1.894.383-3.008.383-2.266 0-5.353.063-7.429-.439-.533 1.888-2.055 6.812-5.068 12.962.151-.073.3-.135.435-.207 3.717-1.952 10.861-5.064 11.162-5.199l5.643-2.452-2.89 5.435c-.067.118-6.264 11.773-10.059 19.383-3.769 7.538-10.835 9.179-15.065 10.151-.637.151-1.241.291-1.733.425-3.035.854-9.18 2.319-14.599 3.623-.064.016-.13.033-.197.042a64.057 64.057 0 0 1-10.955 5.411c-14.568 5.518-29.923 5.208-43.844.092a647.05 647.05 0 0 1-9.193 1.097 45.12 45.12 0 0 1-4.985.291c-13.264 0-20.294-6.736-25.425-13.331-5.493-7.062-12.212-17.546-12.497-17.985L31 158.426l6.585 2.961c3.152 1.419 12.524 5.757 15.205 7 .217-.061.43-.124.642-.186-4.457-6.357-8.112-13.605-10.695-21.634-2.195.662-5.576 1.175-8.206 1.175-.961 0-1.822-.072-2.484-.228-1.471-.336-3.148-1.754-5.431-9.795-1.325-4.668-2.314-9.764-2.603-12.387-.57-5.121-.466-7.864 1.662-9.636 1.283-1.071 5.611-3.344 6.507-3.809l-.192-1.58c-13.75 8.08-21.991 15.22-22.157 15.366L0 134.302l7.005-11.047c5.544-8.755 11.948-15.832 17.84-21.284-.244-.098-.471-.196-.71-.294l-1.299-.585a34.907 34.907 0 0 1-1.34-.653l-.461-.237-.295-.457c-.166-.249-.238-.388-.347-.574l-.29-.512c-.181-.347-.358-.684-.518-1.025a30.878 30.878 0 0 1-.89-2.071 44.74 44.74 0 0 1-1.404-4.198 54.745 54.745 0 0 1-1.62-8.605 54.664 54.664 0 0 1-.259-8.733c.078-1.455.218-2.909.419-4.354.104-.725.213-1.45.358-2.17.15-.734.296-1.418.518-2.221l.155-.564.404-.317c2.294-1.802 4.768-3.163 7.284-4.369a78.87 78.87 0 0 1 6.311-2.616c5.943-16.493 16.162-31.118 29.591-41.311C74.337 5.57 90.664 0 107.671 0s33.334 5.57 47.218 16.106c13.43 10.193 23.649 24.819 29.588 41.307a78.282 78.282 0 0 1 6.316 2.62c2.515 1.206 4.99 2.567 7.283 4.369l.404.317.156.564c.227.803.372 1.487.517 2.221.146.72.26 1.445.357 2.17.203 1.443.348 2.897.419 4.352zm-11.995 48.031c.456-5.052.058-6.139-.455-6.554-.513-.43-2.247-1.412-3.935-2.329l-2.779 19.464a1.39 1.39 0 0 1-.58.942l-3.977 2.781c-.315 1.593-.429 2.345-.817 3.903 2.273.528 5.999.938 7.775.595 1.612-1.748 4.214-12.61 4.768-18.802zm-5.161-17.648l2.977-11.29c-4.318-.978-12.27-2.615-23.1-4.148-5.53 3.976-11.582 7.155-17.53 9.691 18.199 1.771 31.57 4.406 37.653 5.747zm-4.68 27.237l3.385-23.676a240.127 240.127 0 0 0-5.731-1.169l-3.059 21.422a1.415 1.415 0 0 1-.575.943l-11.472 8.023c-.27.192-.616.28-.947.243l-34.572-3.929a1.391 1.391 0 0 1-1.176-.973l-6.227-20.5c-1.668-.431-5.949-1.43-9.696-1.43-3.764 0-8.041.999-9.708 1.43l-6.228 20.5a1.388 1.388 0 0 1-1.025.947l-34.572 7.692a1.483 1.483 0 0 1-.306.033 1.36 1.36 0 0 1-.792-.25l-11.467-8.029a1.396 1.396 0 0 1-.585-.968l-3.091-25.072c-1.284.249-2.443.487-3.479.703-.734.405-1.46.809-2.174 1.213l3.281 26.568 16.666 11.675 42.047-9.354 6.207-20.449a1.389 1.389 0 0 1 1.108-.975c1.574-.253 2.95-.382 4.116-.382 1.153 0 2.536.129 4.105.382.528.083.957.461 1.108.975l6.366 20.956 42.282 4.808zm-8.07-4.411l2.992-20.948c-8.439-1.536-20.78-3.394-35.897-4.554-13.647 4.707-25.077 6.108-25.766 6.155l-.797.057c4.353.374 8.454 1.544 8.66 1.605.452.135.804.481.944.933l6.186 20.366 33.138 3.764zm2.303 11.845l-1.404.983-3.779 2.651-4.095 2.868c-.279.192-.621.28-.954.243l-40.746-4.633-2.966-.337a1.39 1.39 0 0 1-1.171-.977l-6.377-20.998c-1.066-.145-2.014-.219-2.81-.219-.809 0-1.751.073-2.817.219l-6.192 20.392a1.383 1.383 0 0 1-1.025.946l-43.435 9.672c-.103.02-.206.03-.305.03-.279 0-.559-.083-.798-.253l-1.578-1.098-4.726-3.307v-.011l-1.91-1.335c.135.43.289.85.441 1.268l-.006.006c.368 1.092 4.028 11.622 11.467 21.929a873.96 873.96 0 0 0 17.057-5.234c4.488-6.917 10.877-13.777 15.418-14.014a51.12 51.12 0 0 1 2.402-.061c2.221 0 4.344.16 6.31.393-1.671-1.517-2.013-3.298-2.256-4.085 0 0 5.793 4.53 13.17 3.584 7.378-.953 11.959-5.204 11.959-5.204s-.021 3.102-3.236 5.503c6.182-1.869 8.739-2.511 10.489-2.511 1.931 0 2.883.808 3.717 1.519.161.129.322.268.507.419a3519.302 3519.302 0 0 1 12.141 9.614c1.936.227 8.075.926 11.943 1.283 2.23.201 4.245.372 6.217.372.637 0 1.233-.026 1.797-.063 2.558-4.88 4.857-10.411 6.808-16.653.261-.96.516-1.928.743-2.901zm-15.034-51.593c-.01-.006-.02-.012-.031-.012a551.624 551.624 0 0 0-9.826-.651 905.6 905.6 0 0 0-13.667-.668 72.95 72.95 0 0 1-1.574 2.225c-2.479 3.355-7.398 9.51-13.704 14.729 8.926-1.6 24.409-5.56 37.803-14.905.336-.238.668-.486.999-.718zm-29.876.926c.377-.471.729-.926 1.044-1.34-3.281.331-6.512.808-9.67 1.408-10.814 2.024-20.801 5.389-29.11 8.837a383.259 383.259 0 0 1 18.54-.455c3.908 0 7.708.067 11.404.176 3.179-3.056 5.861-6.182 7.792-8.626zm3.587 102.085c-4.503-.332-8.598-2.205-11.903-5.477a271.86 271.86 0 0 0-.502-.512 44.25 44.25 0 0 1-4.881.704c-.698.026-1.361.087-2.091.087l-1.083.011-.413-.011c-4.396 6.539-14.159 7.813-14.605 7.87-.403.046-.734.103-1.191.186 5.442 1.491 10.996 2.138 16.474 1.77 5.492-.367 12.627-1.558 20.195-4.628zm-17.4-7.461a45.604 45.604 0 0 0 3.184-.378 138.958 138.958 0 0 1-3.568-3.857 398.441 398.441 0 0 1-1.92 4.339h.243c.658.001 1.378-.071 2.061-.104zm-3.354-78.632c1.827-1.103 3.582-2.366 5.249-3.712a422.33 422.33 0 0 0-7.278-.072c-10.137 0-19.606.415-28.189 1.061-8.61 4.209-13.875 7.672-13.998 7.76l-8.268 5.514 5.679-8.149a52.452 52.452 0 0 1 2.956-3.857c-9.536 1.066-17.477 2.329-23.41 3.422l3.038 24.632 10.453 7.321 33.184-7.378 6.212-20.464c.104-.337.331-.621.627-.793.098-.063.202-.109.315-.14.192-.052 3.51-.999 7.336-1.465zm3.816-18.788c-2.31-.036-4.623-.057-6.933-.062h-.005c-3.39.005-6.787.041-10.189.109l-6.269 2.971c-.005.005-.041.021-.088.048-.942.46-9.174 4.613-16.919 12.021 6.943-3.65 17.146-8.418 29.153-12.115a144.186 144.186 0 0 1 11.25-2.972zM70.251 98.761c3.251-3.225 6.605-5.886 9.567-7.967-11.415 2.651-21.923 6.543-31.128 10.778a360.846 360.846 0 0 1 21.561-2.811zm2.159-9.949a150.122 150.122 0 0 1 11.813-2.796c-5.798.212-11.6.481-17.393.808-3.366.186-6.715.414-10.065.667-1.678.129-3.345.263-5.007.445-.476.046-.942.098-1.418.16-4.369 2.614-21.127 13.134-32.631 26.889 11.179-7.769 30.654-19.443 54.701-26.173zm-30.85 54.197a68.861 68.861 0 0 1-.621-2.102l-5.162-3.612a1.391 1.391 0 0 1-.586-.969l-2.516-20.449c-1.864.999-4.017 2.225-4.592 2.707-.497.409-.875 1.46-.336 6.332.668 6.01 3.712 17.333 5.348 18.979 1.968.347 6.327-.258 8.465-.886zm-3.815-51.36a229.005 229.005 0 0 0-7.051 1.47l.829 3.127a103.93 103.93 0 0 1 6.222-4.597z" id="jkpath95" fill="#2d3136"/></g></g></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="tune" xmlns="http://www.w3.org/2000/svg"><path d="M6.85 2.852h-2v6h2v-6m12 0h-2v10h2v-10m-16 10h2v8h2v-8h2v-2h-6v2m12-6h-2v-4h-2v4h-2v2h6v-2m-4 14h2v-10h-2v10m4-6v2h2v4h2v-4h2v-2h-6z" fill="#fbc02d" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 50 50" id="twig" xmlns="http://www.w3.org/2000/svg"><path d="M9.727 47.556c-.125-.223-.297-2.168-.183-2.087.034.025.171.267.304.537.132.27.282.487.332.482.123-.011.075-1.196-.1-2.454-.331-2.398-1.176-4.435-2.358-5.69-.2-.212-.344-.4-.319-.419.093-.067 1.327.843 1.842 1.359.293.293.735.825.981 1.181.328.474.465.618.51.534.078-.147-.21-9.903-.376-12.701-.074-1.255.063-1.023.61 1.035 1.064 4.006 1.858 7.922 2.342 11.55.086.637.173 1.172.195 1.19.022.016.092.001.157-.034.888-.483 1.524-.667 2.55-.736.727-.048.945.062.35.178-1.15.222-1.99 1.013-2.344 2.201-.315 1.061-.327 2.707-.024 3.434.152.366.037.426-1.067.56-.716.088-.977.096-1.202.037-.356-.092-1.118-.098-1.195-.008-.031.036-.243.066-.47.066-.38 0-.423-.017-.535-.215zm1.974-3.233c.152-.205.072-.41-.204-.522-.225-.09-.263-.088-.437.025-.21.137-.252.43-.08.554.18.13.607.096.72-.057zm1.248.086a.763.763 0 0 0 .214-.203c.241-.33-.352-.622-.745-.366-.406.265.08.785.531.569zm2.288 3.094c-.033-.039.117-.387.334-.775.216-.387.411-.665.433-.618.07.152-.201 1.28-.33 1.372-.15.108-.354.117-.437.02zM8.2 47.092c-.29-.343-.221-.434.14-.182.176.123.321.263.321.31 0 .165-.279.087-.46-.128zm8.649-.145c0-.053.102-.18.227-.282.25-.204.312-.113.143.207-.095.18-.37.236-.37.075zm8.065-.827c-.243-.025-.48-.088-.527-.141-.11-.125-.114-3.043-.004-3.043.045 0 .132.149.193.331.127.38.228.42.31.124.094-.337.065-3.472-.039-4.297-.449-3.55-1.865-6.124-4.342-7.89-1.086-.774-2.653-1.436-4.047-1.711-.764-.15-.522-.224.598-.182 2.364.089 4.167.706 5.847 2.001a11.046 11.046 0 0 1 2.32 2.502c.453.682.64.854.64.584 0-.07.063-.882.139-1.805.679-8.26 2.396-15.1 4.984-19.86 1.86-3.422 5.108-6.817 7.885-8.244 1.397-.718 2.539-.988 4.02-.952.933.023 1.01.036 1.77.307a6.822 6.822 0 0 1 1.363.662c.612.407 1.309 1.004 1.235 1.058-.026.018-.343-.165-.705-.407-2.657-1.771-5.062-1.52-7.12.742-1.108 1.22-2.651 3.53-3.634 5.443-2.828 5.503-4.541 11.464-5.291 18.413-.163 1.509-.282 3.76-.195 3.703.032-.022.266-.52.518-1.108 1.597-3.723 3.578-6.428 5.79-7.908.672-.449 1.612-.904 1.715-.83.022.016-.172.22-.432.454-1.957 1.754-3.248 3.76-4.232 6.572-.938 2.68-1.366 5.588-1.368 9.3-.002 1.741.188 4.385.366 5.101.125.505.08.546-.585.546-.55 0-2.306.138-3.416.27-.414.05-.817.04-1.609-.036-.58-.056-1.129-.119-1.218-.14-.165-.037-.18-.014-.2.302-.01.186-.098.203-.728.139zm2.507-6.725c.294-.11.375-.22.375-.517 0-.63-1.309-.706-1.524-.088-.074.211.13.51.42.616.297.108.413.106.73-.011zm2.369-.052c.277-.222.318-.364.174-.611-.4-.691-1.755-.307-1.428.404.121.266.299.35.738.354.227 0 .387-.045.516-.147zm3.011 6.681c-.027-.05.088-.268.256-.484.879-1.135 1.22-1.544 1.284-1.544.04 0 .056.037.036.082l-.423.964c-.212.485-.445.924-.519.977-.169.122-.57.125-.634.005zm2.446-.596c0-.121.853-.683.896-.59.018.04-.056.209-.166.376-.168.259-.238.305-.464.305-.164 0-.266-.035-.266-.091zm-13.04-.124c-.177-.159-.493-.656-.462-.725.018-.038.248.1.512.309.264.207.457.405.428.438-.075.088-.371.074-.478-.022z" fill="#9bb92f" stroke-width=".078"/></symbol><symbol viewBox="0 0 500 500" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="typescript" xmlns="http://www.w3.org/2000/svg"><path d="M49 51h408v408H49V51zm246.669 314.879l19.463-1.702c.922 7.8 3.067 14.199 6.435 19.198 3.368 4.998 8.597 9.04 15.688 12.124 7.09 3.085 15.067 4.627 23.93 4.627 7.87 0 14.819-1.17 20.845-3.51 6.027-2.34 10.512-5.548 13.455-9.625 2.942-4.077 4.413-8.526 4.413-13.348 0-4.892-1.418-9.164-4.254-12.816-2.836-3.651-7.516-6.718-14.039-9.2-4.183-1.63-13.436-4.165-27.759-7.604s-24.355-6.683-30.099-9.732c-7.445-3.899-12.993-8.739-16.644-14.517-3.652-5.779-5.478-12.249-5.478-19.41 0-7.871 2.234-15.227 6.701-22.069 4.467-6.842 10.99-12.036 19.569-15.581 8.58-3.546 18.116-5.318 28.61-5.318 11.557 0 21.75 1.861 30.577 5.584 8.828 3.722 15.617 9.199 20.368 16.432 4.75 7.232 7.303 15.421 7.657 24.568l-19.782 1.489c-1.064-9.856-4.662-17.301-10.795-22.335-6.133-5.034-15.191-7.551-27.174-7.551-12.479 0-21.573 2.286-27.281 6.86-5.707 4.573-8.561 10.086-8.561 16.538 0 5.602 2.021 10.21 6.062 13.826 3.971 3.617 14.34 7.321 31.109 11.115 16.769 3.793 28.273 7.108 34.513 9.944 9.076 4.183 15.776 9.483 20.101 15.9 4.325 6.417 6.488 13.809 6.488 22.175 0 8.296-2.375 16.113-7.126 23.452-4.751 7.338-11.575 13.046-20.474 17.123-8.898 4.077-18.913 6.116-30.045 6.116-14.11 0-25.933-2.056-35.47-6.169-9.537-4.112-17.017-10.299-22.441-18.559-5.424-8.26-8.278-17.602-8.562-28.025zm-65.728 50.094V278.454h51.583v-18.399H157.938v18.399h51.37v137.519h20.633z" fill="#0288d1"/></symbol><symbol viewBox="0 0 500 500" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="typescript-def" xmlns="http://www.w3.org/2000/svg"><path d="M457 459H49V51h408v408zM69 71v368h368V71H69z" fill="#0288d1"/><text x="342.219" y="344.544" font-family="ArialMT" font-size="12" fill="#0288d1" transform="translate(-6058.94 -5838) scale(18.1514)"><tspan style="-inkscape-font-specification:sans-serif" font-family="sans-serif" font-weight="400">TS</tspan></text></symbol><symbol viewBox="0 0 24 24" id="url" xmlns="http://www.w3.org/2000/svg"><path d="M16 6h-3v1.9h3a4.1 4.1 0 0 1 4.1 4.1 4.1 4.1 0 0 1-4.1 4.1h-3V18h3a6 6 0 0 0 6-6c0-3.32-2.69-6-6-6M3.9 12A4.1 4.1 0 0 1 8 7.9h3V6H8a6 6 0 0 0-6 6 6 6 0 0 0 6 6h3v-1.9H8c-2.26 0-4.1-1.84-4.1-4.1M8 13h8v-2H8v2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="verilog" xmlns="http://www.w3.org/2000/svg"><path d="M17.282 17.08H6.718V6.513h10.564m4.226 4.226V8.627h-2.113V6.514c0-1.173-.95-2.113-2.113-2.113H15.17V2.288h-2.113v2.113h-2.112V2.288H8.83v2.113H6.718c-1.173 0-2.113.94-2.113 2.113v2.113H2.492v2.113h2.113v2.113H2.492v2.113h2.113v2.113a2.113 2.113 0 0 0 2.113 2.113H8.83v2.113h2.113v-2.113h2.112v2.113h2.113v-2.113h2.113a2.113 2.113 0 0 0 2.113-2.113v-2.113h2.113v-2.113h-2.113V10.74m-6.339 2.113h-2.112V10.74h2.112m2.113-2.113H8.831v6.34h6.338z" fill="#ff7043" stroke-width="1.056"/></symbol><symbol viewBox="0 0 24 23.999999" id="vfl" xmlns="http://www.w3.org/2000/svg"><defs><style>.jra{fill:#f05223}.jrb{fill:url(#jra)}</style><radialGradient id="jra" cx="205.45" cy="208.29" r="225.35" gradientTransform="matrix(.04556 0 0 .0456 2.888 2.88)" gradientUnits="userSpaceOnUse"><stop stop-color="#ffd104" offset="0"/><stop stop-color="#faa60e" offset=".35"/><stop stop-color="#f05023" offset="1"/></radialGradient></defs><title>houdinibadge</title><g stroke-width=".046"><path class="jra" d="M19.97 3H4.03A1.03 1.031 0 0 0 3 4.031v4.135C4.548 6.977 6.563 6.21 8.948 6.21c5.107.003 8.35 3.574 8.348 8.081 0 3.13-1.46 5.485-3.746 6.71h6.42A1.03 1.031 0 0 0 21 19.968V4.031a1.03 1.031 0 0 0-1.03-1.03z" fill="#f4511e"/><path class="jrb" d="M3 17.722v2.247A1.03 1.031 0 0 0 4.03 21h1.837C4.474 20.21 3.49 19 3 17.722z" fill="url(#jra)"/><path class="jra" d="M8.948 8.231c-2.586-.09-4.598.86-5.948 2.264v3.163c.918-2.654 3.447-3.87 5.565-3.85 2.647.027 4.689 2.025 4.7 4.284.012 2.159-.892 3.748-3.33 4.14-1.33.213-3.411-.567-3.318-2.578.046-1.037.854-1.622 1.777-1.58-.905 1.213.293 2.102 1.139 1.921 1.048-.224 1.475-1.156 1.475-1.878 0-.762-.718-1.994-2.498-1.951-2.204.052-3.591 1.639-3.638 3.602-.056 2.468 2.253 4.091 4.622 4.121 3.48.046 5.543-2.24 5.539-5.586-.005-3.029-2.434-5.946-6.085-6.072z" fill="#f05223"/></g></symbol><symbol viewBox="0 0 24 24" id="virtual" xmlns="http://www.w3.org/2000/svg"><path d="M21 14H3V4h18m0-2H3c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h7l-2 3v1h8v-1l-2-3h7a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 281.25 281.25" id="visualstudio" xmlns="http://www.w3.org/2000/svg"><path d="M196.18 101.74l-52.778 42.444 52.778 40.889V101.74m-136.67 110l-30-18.889v-100L62.843 81.74l47.778 37 96.666-89.222 44.444 27.778v172.22l-55.555 22.222-85.111-81.555-51.555 41.555m3.333-48.889l20.667-19.111-20.667-19.778z" fill="#ab47bc" stroke-width="11.111"/></symbol><symbol viewBox="0 0 300 300" id="vscode" xmlns="http://www.w3.org/2000/svg"><defs><style>.icon-canvas-transparent{fill:#f6f6f6;opacity:0}.icon-white{fill:#fff}</style></defs><title>BrandVisualStudioCode</title><path d="M218.62 29.953l-105.41 96.92L54.301 82.47 29.955 96.64l58.068 53.359-58.068 53.359 24.346 14.212 58.909-44.402 105.41 96.878 51.424-24.976V54.93zm0 63.744v112.6l-74.719-56.302z" fill="#2196f3" stroke-width="17.15"/></symbol><symbol viewBox="0 0 24 24" id="vue" xmlns="http://www.w3.org/2000/svg"><path d="M1.821 4.15l10.21 17.618L22.24 4.235V4.15h-7.692L12.113 8.33 9.691 4.15H1.82z" fill="#41b883"/><path d="M5.937 4.15l6.152 10.616 6.18-10.617h-3.722l-2.434 4.179-2.422-4.179H5.937z" fill="#35495e"/></symbol><symbol viewBox="0 0 420 419" id="watchman" xmlns="http://www.w3.org/2000/svg"><g stroke="#fff" stroke-linecap="round" stroke-linejoin="bevel"><path d="M166.95 145.32a93.935 123.23 0 0 1 92.934 3.263" fill="none" stroke-width="18.467"/><path d="M162.92 137.96L44.63 256.25a174.07 173.93 0 0 0 5.705 16.486l123.68-123.68-11.096-11.096zM266.54 144.04l-11.096 11.096 117.16 117.16a174.07 173.93 0 0 0 5.691-16.5l-111.76-111.76zm170.65 170.65v22.193l17.1 17.1 11.096-11.098-28.195-28.195z" fill="#fff" stroke-width="1.963"/><path d="M167.52 273.36a93.935 123.23 0 0 1 92.934-3.263" fill="none" stroke-width="18.467"/><path d="M49.516 144.56a174.07 173.93 0 0 0-.809 2.213 174.07 173.93 0 0 0-4.757 14.344 174.07 173.93 0 0 0-.016.055l119.56 119.56 11.098-11.096-125.07-125.07zM454.87 64.703l-17.668 17.668v22.191l28.764-28.764-11.096-11.096zm-80.984 80.984l-117.86 117.86 11.098 11.096 112.18-112.18a174.07 173.93 0 0 0-5.416-16.777z" fill="#fff" stroke-width="1.963"/></g><image x="21.229" y="20.262" width="378" height="377.1" preserveAspectRatio="none" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAaQAAAGjCAYAAABjSWGNAAAgAElEQVR4AeydB3hUVdrH/+fOpNMF JAFUmivFXtZuIBRRQUUTil1RV3et6Lr6rSu6rg3B1dXVtXeBCCioCASIimLDhkFsgAIJSAkhPZmZ 8z3vjReGMJlMueeWmfc88MzMLaf8zp3855zznvcV4MQEkoxA7sVr01PrUrv4pdZNBNA1AHQRkJ0g ZAcJrYOA7Aipv8/Q6JgUKdBkG0iZISDSDVwSaAfAY3z+/bVSAD56L+mfwA5INAqgSkrUCKA2IOQO IcQOIUU5pNwhNZQLia2Q2OyH3OJBxqaiwk4VzfLlj0wg4QmIhG8hNzDJCEgxJH9DjgfevgGBAyCx vxSyh5DoAWA/CHQH0MEFUOoBbJbAeiHEBiED6wMSv0DgF02In1J2Vq+ZP78fXcOJCSQMARakhOnK 5GrICaO3tE3P8B0kERgkgP6Qop8E+gqgL4Bdo5gEphKAwHpIrIHAjxL4DpCrAo1yVfHsHhsSuN3c tAQmwIKUwJ2bGE2TYsjY33qLQOBwCHk4JA6HhgE08kmM9ilpRQUgvwPE1xD4MiC0r2R67dfFz/eq U1IaZ8oETCLAgmQSSM7GHAIj8td38sN7HAROkpDHATgMTWs15hSQvLn4JLBaQHwqEPgQQiwvmpHz XfLi4JY7kQALkhN7JYnqlDd2w4GQnuMk5AkCOBHAQQD4ubTgGZDAFg1ieQCBj6Dhg7SKmhW8LmUB eC6iRQL8xW8RDZ8wn4AUQ8eVHib9YjCELj4nAOhqfjmcY4wE6iGxAkIsCwDvSel5v7iwa1WMefFt TCBqAixIUSPjG6IhkDtmQw/N4xkKTQ4TEkNZgKKhZ/u1DZD4CAKLIMSik/p3WzF5sgjYXiuuQMIS YEFK2K61p2GTJ0tt2Xebj5EyMArAab+vAdlTGS7VVAI0xSeAd6QQb2uBtIW8V8pUvJwZz9XzM2AG AdpoKqpTh3uEdlYA8gwBdDEjX87D0QQaALlMCLzha5Rz2NTc0X3lmsrxCMk1XeWsig6/YFOWr0Ge JoBzICWNhNo6q4ZcGwsJkFOKT4TUZiOgzSqate8aC8vmohKIAAtSAnWm6qaMHPljWl2bzBGapk2A lKMBZKguk/N3JYHPpMCrHuGbuWj6fqWubAFX2hYCLEi2YHdToVIMKSg7SQAXABgDoJObas91tZWA H5DvCeCV2rrUwg/ndqm0tTZcuOMJsCA5vovsqeDwszd19af4LwbEZQAOtKcWXGoCEaiCwAwB7emi Gd0+TqB2cVNMJMCCZCJMt2dFFnIflJQNh8BEgKbkRIrb28T1dySBlULIpzyBwCsLCntud2QNuVK2 EGBBsgW7swqlvUIer+cSQE7UPWI7q3pcm8QlUAtgtibFU4sKu70PCJm4TeWWRUKABSkSSgl5DTkt 3XSqJuVfJDAiRFyfhGw1N8qxBH4AxJP+hrpnit/otcOxteSKKSXAgqQUr/My18216wMXCOB6AH9w Xg25RklOoEoI8bzw+R5ZNKvnj0nOIumaz4KUJF0+YsyWbL/Xdx0gr5BAxyRpNjfTvQQCkPIdDdqD iwqz33NvM7jm0RBgQYqGlguvbfKmrd2MJrPtNBc2gauc5ASElJ8EIO4/eWD2m+xLL7EfBhakBO3f wfmlR2oCtwI4G4CWoM3kZiURASmwGpAPVLTPeXnFk6IxiZqeNE1lQUqwrs4bW3Y0AoF/QIjT2Vdh gnUuN8cgsFYK3LNPoPzFwsKBDcZBfnU/ARYk9/eh3oIh+WXHaELeIZs8bCdIq7gZTCAsgXUQuK9T oPw5FqawnFxzkgXJNV0VuqJD8zcdDCHvlvpG1tDX8FEmkOAE1gohJp/Yv9vLvMbk7p5mQXJp/w0Z u7mPkP7JACbwGpFLO5GrbS4BiRII/H3xzOw3eZOtuWityo0FySrSJpWTO760s8cv7wQEeVVINSlb zoYJJBAB+bGQ4uaiwpxlCdSopGgKC5JLuvm4/PUZmcJ7NSD/DqCDS6rN1WQCdhKY45f+vxYX9vzJ zkpw2ZETYEGKnJVNV1L4h9JxAuI+9jNnUxdwsW4mQFZ4//E31N/NLomc340sSA7uo2H5Gw7zC+3f AjjFwdXkqjEBNxDYKgVuPbl/9rNs+ODc7mJBcmDf5J61toMnNfVfgLgCgNeBVeQqMQF3EpD4XGja NRyTyZndx4LkqH6RYkj+pouEkFMAdHZU1bgyTCBxCEgIPFvvabx52av7lydOs9zfEhYkh/Th0HM2 95Ye//8ADHVIlbgaTCDRCWwWENcWzcyemegNdUv7WJBs7qncXOnVum66XoBMuZFpc3W4eCaQdAQE xFyfz//n4tk9NiRd4x3WYBYkGzuEjBak0J6WwJE2VoOLZgJMANgJIW49qX+3J9jowb7HgQXJBva0 pyhLeO+QkJPYaMGGDuAimUDLBJYJgSuKZuR81/IlfEYVARYkVWRbyHfouLJcBPCUhOzbwiV8mAkw AXsJ1EOKe3Z07HYvh7mwtiNYkCzinXvx2nRvTdq9EriOw0JYBJ2LYQLxEfgCUlywuDB7VXzZ8N2R EmBBipRUHNcNGVt6hJB4CcCAOLLhW5kAE7CeQB1tqF0yI/thdtiqHj4LkkLG+fnSUy7KbpWQ/wBE isKiOGsmwASUEpBLPBouXji9+3qlxSR55ixIih6A3DEbemhe7WV2+6MIMGfLBKwnsA3AxMUzc96w vujkKJEFSUE/D87fOFoT4lkA+yjInrNkAkzAVgLyv/7MhknFz/eqs7UaCVg4C5KJnTpy5I9p9W3b TBGQf2HDBRPBclZMwHkEVnr8KFg4K2e186rm3hqxIJnUd0PGlO4vPHgdAkeZlCVnwwSYgLMJVEHI yxfP6D7d2dV0T+1YkEzoq6H5ZadLIV/gKToTYHIWTMBlBIQQj6bsrLpp/vx+9S6ruuOqy4IUR5fo VnRa2Z1S4jaeoosDJN/KBNxP4DMhcW5RYc6v7m+KfS1gQYqRvR6zKCXtVQiMjDELvo0JMIEEIiCB LR4p8hcVZr+XQM2ytCmapaUlSGF5+WUDvKnpn7EYJUiHcjOYgAkEBNAlIAKLho7deI0J2SVlFjxC irLbh+SXni0EXgTQJspb+XImwASShICAeC6lsuoqXleKrsNZkKLglVdQeiuAf/F6URTQ+FImkLwE lnkatXMWzun2W/IiiK7lLEgR8KL9RQ1tsyia60URXM6XMAEmwAQMAmsDHjFq6WvZJcYBfm2ZAAtS y2z0M7njSzt7/JgD4MRWLuXTTIAJMIFQBCqkkAVLZnRfGOokH9tNgI0adrPY611u/vq+Xr9YzmK0 Fxo+wASYQOQE2gsp3h4ytnRi5Lck55UsSC30+5D8smM8wvMhB9JrARAfZgJMIBoCXiHxVN7YjXcC kmemWiDHYEKAyRu76QzIALkDyQpxmg8xASbABGImQBZ4vt+6XVFcLHwxZ5KgN7IgNevYvPyNl0EI MmDwNDvFH5kAE2AC5hCQmJ9Zh3PnzcupMSfDxMiFp+yC+nFIQdlNEOIpFqMgKPyWCTAB8wkIjKxJ xyLy+GJ+5u7NkUdIet9JkVdQRvuLaJ8RJybABJiAVQS+9kvvqcWFXTdZVaCTy0l6QZo8WWrLVpU9 JoE/ObmjuG5MgAkkJgEB8ZNPw7Di6dnrErOFkbcqqQWJvHVvF2UU2fXCyJHxlUyACTAB0wn86pf+ vOLCnj+ZnrOLMkzaNaTcXOndLja9zGLkoqeVq8oEEpfAfh7hfW/4OaUHJW4TW29ZUo6Q8vNLUreh 42tCYEzriPgKJsAEmIBlBDZDiiGLC7NXWVaigwpKOkEiMSoXnQol5GgH9QNXhQkwASagE6C4SprU 8ooKu61MNiRJNWVH03Q0MmIxSrbHnNvLBNxDgOIqSREoGjq2tL97am1OTZNmhEQGDNtE6asCosAc dJwLE2ACTEAlAVkGIXMXz+jxg8pSnJR3UoyQfreme57FyEmPHteFCTCB8ARENqS2mJw8h78ucc4m gSBJUS7KHgVwfuJ0G7eECTCBJCHQwyM8i3LHbOiRDO1NeEEaMrbsHt70mgyPMreRCSQsgQM8Hu3d kfllXRK2hb83LKEFaUjBxluExN8SvRO5fUyACSQ4AYGBjQjMG5q/vX0itzRhBSlvbOmVAuLeRO48 bhsTYALJQ0AK8UcpamfT1pVEbXVCCtLg/I2jIfEYgKSxIkzUB5TbxQSYQDABMWS76Phsogb5SzhB Gjxu0x81IV7jEBLBDzG/ZwJMIIEInJc3tuyhBGrPrqYklCCReaQIBOYByNzVQn7DBJgAE0g0AhLX Dc0vTbj18YSZ0iILlEaBjyRk0tjsJ9p3jNvDBJhAVAQkpBy/uLD7jKjucvDFCTFCokW+Bsg3WIwc /KRx1ZgAEzCbgIAQz9IyhdkZ25VfAgiSFPoin8DxdkHkcpkAE2ACNhHI1AKBuYmycdb1gjS0oOz/ AJxn08PAxTIBJsAE7CbQ1ePV3kmEPUquFqS8/LJzJXCX3U8Dl88EmAATsJnAwVLUv+R2c3CPzRBj Ln5o/qaDIeRbABJ2k1jMcPhGJsAEkpHAH3oPrNTWlkxb6tbGu9LKLu/sDfsgRfsEQB+3gud6MwEm wAQUEJBC4NyiGTmzFeStPEvXTdlRKAmkaLTxlcVI+ePBBTABJuAyAkJKvKDPILms4lRd1wlSudj0 LwDDXMiaq8wEmAATsIJAG6kFZrnRyMFVgkQ+6iTkX63oUS6DCTABJuBaAhL9Aqhznc871wjS0HM2 99aEeIEdprr2K8IVZwJMwEICQmBMXsGmGy0sMu6iXGHUkHvx2nRPddpHEDg87hZzBkyACTCBpCEg G4UUQ4oKc5a5ocmuGCF5a9IeYjFyw+PEdWQCTMBZBESKFGKGbpnsrIqFrI3jBWno2NIxHII8ZN/x QSbABJhABARkDlI8z7lh06yjBWnImNL9IfF0BMT5EibABJgAE2iRgByVl192bYunHXLCsWtItN9o O8reBztNdcijwtVgAkzA5QTqhSaPK5re/UuntsOxI6RyUXo7i5FTHxuuFxNgAi4kkBaQ4tVRo0od G8DUkYI0JL/sGAlBXrw5MQEmwASYgEkEhMRBtZnifpOyMz0bx03ZDb9gU5a/PvAFgANNby1nyASY ABNgAlIKeeqSGd0XOg2F40ZI/nr/AyxGTntMuD5MgAkkEAEhpHh2RP76Tk5rk6MEacjYjcMBcZXT IHF9mAATYAIJRqC7X3gedVqbHDNld8LoLW3T0xu/BbCf0yBxfZgAE2ACiUhASoxZUpgzxyltc8wI KT29cQqLkVMeC64HE2ACyUBACPnYiRN+6eiUtjpCkIbll50C4AqnQOF6MAEmwASSg4DITvN7pzml rbZP2ZFNfE0GvuGAe055JLgeTIAJJB0BiZGLC3Petbvdto+QajPEP1iM7H4MuHwmwASSmoDA407Y MGurIOWNX3+IRMBV8TqS+qHlxjMBJpCoBA6oyRB32N042wRp8mSpwe99AhApdkPg8pkAE2ACTEDe OHjshkPt5GCbIC0r2XQlII+zs/FcNhNgAkyACewi4NWkeEIfLOw6ZO0bWwRpZH5ZFynkPdY2lUtj AkyACTCB8ATEse9/V3Zp+GvUnbVFkBqaxKiDumZxzkyACTABJhATAYl7cs9aa8vfZ8sFadi40qMA 2KbAMXUQ38QEmAATSBICAuiipabfZUdzLRYkKQIB+R8AFpdrB1oukwkwASbgTgIC8uph+RsOs7r2 lgrDkPxNFwHiWKsbyeUxASbABJhAVAQ8AWgPRXWHCRdb5qnhd48MPwDobkK9OQsmwASYABNQTEAI eVbRjO5vKi5mV/aWjZBq0sVNLEa7uPMbJsAEmIDjCUgpHjjyCmnZXlFLBGnEmC3ZEPJmx9PnCjIB JsAEmEAwgQM7lJddHXxA5XtLBMmX0vhPAG1UNoTzZgJMgAkwAQUEBG63KkSFckEafk7pQZC4SAEm zpIJMAEmwATUE9gn1Z/yV/XFWGB+7feARkdeKxrDZTABJsAEmID5BITENXnjN+9rfs575qh0hPT7 Jthz9iySPzEBJsAEmIDLCGSJQODvquusVJACAZC/OstMy1XD4vyZABNgAslKQEp5xfD8Tb1Utl+Z IA0pKD0ZwDCVlee8mQATYAJMwDICqX7NTwFVlSVlgiQgbQ/2pIwaZ8wEmAATSEYCUpw/ZOzmPqqa rkSQ8saVngSIIaoqzfkyASbABJiALQS8Aj5la0lKBAkBOdkWVFwoE2ACTIAJqCUgxflDz9ncW0Uh pgvS0PzSE3l0pKKrOE8mwASYgCMIeOFRY3FnuiBB4FZHIONKMAEmwASYgBICEvK84eM29jQ7c1MF KW/8+kMkMNLsSnJ+TIAJMAEm4CgCqb6AuMHsGpkqSPB7yL0E7zsyu5c4PybABJiAwwgI4PIR+es7 mVkt0wQpd1zZAQDGmlk5zosJMAEmwAQcS6CNT3j/bGbtTBMkLYAb2WedmV3DeTEBJsAEnE5AXntc /voMs2ppitPT3LPWdhCQl5hVKc6HCdhNwOsVaN9WQ1amQFaGQEaGhox0gdQUAaEBaali19x0Q6PU j1OdG30S/gDQ0CDh9wPVtQFUVUlUVgdQXRNATa20u2lcPhMwk0DnLOE5H8BTZmRqiiBpqekTAcnx jszoEc7DUgIkLL3396JvrxT03T8FPXK8yNnXgy77eJTUo7pG4tdSH9Zv9OGXDT78vK4Rq39uRFV1 QEl5nCkTUE1AAtcD8mlAxP1rK24DhNxc6fV0LfsZwH6qG875M4F4CXTqoOHQAWk4dGAqBv0hFQf0 NOU3WbzV0gXqu58asXJ1A75YWY/NW/xx58kZMAGrCAjg1KKZOQviLS/ub6O3a9mZksUo3n7g+xUR SEkROKR/Kv54eJr+v3t23I+8kpr27O4F/R9+StN0/MZNPnzxTQM++7oeK76pR31D3D8+ldSbM2UC REBKXAUgbkGKe4SUl1+6FAK53C1MwCkEaK3n2CPSMfiEdBx/VLpTqhVXPZZ9WocPP6vDxyvq9fWo uDLjm5mA+QQCwu/pVzRr3zXxZB2XIOXllw2AkCXxVIDvZQJmEKCR0PFHpWHw8Rk48ZjEEKGWuJAw LXq/Fp98WY/GRh45tcSJj1tLQEDcXzQz+2/xlBqfIBVsfBQQptqhx9MYvjf5CPQ5IAUjB2fgrFOz kq/xAN4qqsE7i2vww5rGpGw/N9pRBLZ2kuXdCwsHNsRaq5gFadSo0syaDGwE0CHWwvk+JhALAbKM yzsxA6OGZ6Jfr5RYski4e0iQ5i2qwdIPa1FXz6OmhOtglzRISHleUWH3V2OtbsyCNLRg46US4plY C+b7mEC0BPbt4sGoYZkYdybvMAjHbtbb1Zi7qAYby3zhLuNzTMB8AhIfLC7MoWjhMaWYBElKeeCM udULXp1TeQDtq+DEBFQSGHRQKkYPy8SQE03bEK6yuo7J+73ldZi7sBpfr4p5BsUxbeGKuIOApkE+ PLnTjQMOSv93LDWOWpBIjAB8T4XNW1iDp1/bCRalWNDzPa0ROO7IdH1a7pjD0lq7lM+HIfDNqgZ9 xFT8UW2Yq/gUE4iPAE2fP35fZyOTvwsh/mV8iPQ1FkG6F8AuSwoWpUhR83WREsg9PkMfER0yIDXS W/i6CAjQxtt5C6t1Cz3JExsREONLIiXQe78UPDlllxgZt3mFEFHt8I5KkKSU5E9lr4lpFiWDP7/G Q+CUY2lElIXDBrIQxcMxknsfeqoCbxfVRHIpX8MEwhJoQYzontOEEPPD3tzsZLSCdCqAkAWQKP3v 5Z1s4dMMMH9snQBNyZHFHE3RcbKOwKofGnTLPNrTxIkJxEIgjBhRdjOFEFGFJIpWkF4EcEFLFSdR eviZipZO83EmsAeBA3unYPTwTJw6OHOP4/zBWgLkmohMxskbBCcmECkB8gP59INdWru8vRBiZ2sX GecjFiQpJf3VqDZubOmVRaklMnzcINCxvaabbp9zuns3s1IYCWMdJjNDoLau6XNKCnaFojDa65ZX GimRVd53P/ImW7f0mV31pC0Y40a30Wc2WqnDJUKI51u5ZtfpaASJhl7Td90Z5g2LUhg4SXxKCGD8 WW1w6bi2jqZAISHWl/qwcZMf5OR0yzY/tu8IoKIygIqdgV1CFK4RXg/QsYMHnTtq6NTRg25dPdi/ hxf75XhBZuxOTrSPqfCtamzdHtV6tJObxHUzkUAUYkSlLhJCDI+0+GgE6U0AoyPNmEUpUlLJcR15 Vrj1Guc59aA/urRP59vVDbr7nTW/+pT7hyNh7pHtxYADU3BQ31TdiKNnjvO8kD8/sxKvzK6KSICT 4ynmVkYpRgawbCHEJuNDuNeIBElK2RHA9nAZhTrHohSKSnIdoz+8Z52a6Shfcx9/UY9PvqjDFysb 9BGQE3qka2cPjjg4DUcflgayNnRSuu/RHSj6gA0fnNQndtQlRjGiql4jhHg0kjpHKkgUnvzZSDJs fg2LUnMiyfP5vDFtcMlYZ0zP0R/UpR/V4qtvGxwfW4g8lx91SBpOOCYNp+Y6w+CDnLjSd/nnX3h9 KXm+wbtb2r6thosL2kayZrT7pt3vioUQg3d/bPldpIL0NtmUt5xN+DMsSuH5JNpZp3hYIKuxhe/V 6kHu3BqmgcSJggtS4D4nxHZ66fUqvPh6JU/jJdqXNkx7sjIFJo5vF6sYGTnvK4T4zfjQ0murgiSl bA9gR0sZRHqcRSlSUu69zuMBLhvXDgWj7bOeW7fBh/lLavQpJjJASKTUrq2GYSdl4KqL2tnerH8+ VI73PmYzcds7QnEFTBIjquWVQognW6tuJII0HkDM7sSDK8CiFEwjsd4POzkDt/zZPqMFGg3NXViD L1bWJxbYEK0ho4hDB6TqXi3sXG+a/U41XplTpVsehqgmH3I5ARPFiEgsFEKMaA1JJII0E0B+axlF ep5FKVJS7riOwoVfPLYtzjnNnlHRzHnVmLeoGmWbk9NEOWdfjy5M+WfYw5+e0keeqdB/DLjjieVa RkKA9tZdPiHuabrmRXUQQoT1nNCqIAUCcqYQ5gkS1ZBFqXk/ufOznaOiFwsrMXt+DaqqE2taLtYn gf6AnD0yyzYjkjcX1GD2/GqOwRRrBzrsvusuax/vmlGoFh0phPgi1AnjWKuCNOutquvHnJ71kHGD Wa8sSmaRtD4fWiuiX0/n2vCr/IXCSsycW+14Sznre6WpRK9XYMxpmbjiPHvWmZ54aSdef6tVhy52 4eFyIyCgSIyoZE0IEdbPPHnvDpu2+f5y7uqfGo/PO8nc4Gh/6JOCju09+OTLxJ/zDwvYZSfJ0uvZ aV0w4EBrvQ2QEP3ffeX63iF/cs7ORfSkBAJAyfeN+tpOba3EkYdYG0vqqEPT0KG9B6Wb/dhZyaPX iDrNQRepEqM/3bK14sWnMqesWnVnfILUa9Ckh0s3+Tuv/qkRLEoOenJsqArtKbpuIhldWpdmzK3C P6aU47Ov6sFCFDl38rNX8kMjXn2jGo0+icMHWSdM9GPzrFOzUF0r2S9e5F1m+5WqxOiav28lLyjp dahatGbV1F/DNTTsCGnI2M19hJR3UQbk14tFKRzKxD3X94AUfVOclc5Q58yvxgOPV2Dph3U8PRfH o0UjppXfNWDGvGpoAjjYQj96Rx+ahvbtPPh5nU93PhtHM/hWxQRUidH1d2zDqh9+30wtsGltydQl 4ZoSVpB6DbxxrADOMDIgUfplgw+nHMfTdwaTRH+lX7p33dwR/XqnWNLU5Svq8PgLO/HGuzU85WMi cRpdfvltAxYva9o71L+fNVOuB/VNAVkA/rYtgJ/WsZcHE7vUtKyuubQ9Ro8w3yMIiRH5iAxKaWtL pj4d9Hmvt2GNGobkl84SAmOa30V7H26/gdzbmZvY0MFcnvHmRhswrTTnfvQ5EiJnLoiTFdthA9NA 01H7dfcip5sHbTI1tMnSdMwNjVIfBZBn8A2lPtAG3a9K6rH2170CLMfbLabcT/uYRg/PwinHWec3 j4wdyOiBk3MIkBidaY0YUaP9XunvuqCwZ4t+UVsUpPx86dkuyrYCCLnbUZUokfnof54Na6runN5M 0JrQegMFzjvpj9b8saJ1ohdmVoH+qDsppaUKDD4hA8NPzsAhA2IbUWwvD+CDT+vwzmJn+oEbNSzT 0nXBjz6v04MB0pogJ3sJqBKjG+7YhpV7jox2N1SK/MWF2a/vPrDnuxYFKS9/w3EQ2kd7Xr7nJxal PXkkwieaXrnyAmtMht9bTt4VqvXwD05iRzvUzz29DS44t42p1fr863p9Ayn9UXZS0jToa4QTzja3 veHa+NyMptAW4a7hc+oIqBKj2+7djk/D/dgQeHLxjJwrW2pZi2tIvQbedJEQGNLSjXSc1pNUrCnR vDMthn7KJuHh8Jt+jqboLjjXGu/cT76yU18r2rzFOTbcNCKiP8r33baP7prHbMA53bz6iOsPfVJR XRvAxjJntJ0s8mh9icJy0B6zvr3UrxfSKJy+4zSlSdF3OVlH4M8Xt1MSDqZVMWpqYoe1JVMfaam1 LY6QhuaXLpACEUX645FSS3jdcZz2FNHUDXleUJ30Hf3vVDsmDhG1lzaTjh1tvZcDGimR/z0aOTkp 0ZoC/YK2Kt37nx1YvIzjLVnBm8SIPHqYnSIUI71YKf09lhT23BiqDiFHSLm50ivaVP0XQEQT5zxS CoXWHcdGDc/EnZM6os/+6n8VP/x0BV6aVYXKKmdsmKTRwLgz22DaHftYuk/HeDIoSuzQkzJ0I4kd lQE4ZbT4/c+NmPVONVJThCUboGmt0uMR+KpkD4ssAxO/mkTACWJETdEgVqxZNW1lqGaFFKRex151 tJDy6lA3tHSMRaklMs49ftn4tpg4Qf16EVnO0eZWChXuhETesmmt7N93ddajtNpdpwN6pmBEbia6 dfVie7kfW7fbL9iNjdBHbr+W+nGyBRFsDx63vWsAACAASURBVOmfqntuIevE6hqewjP7mVQlRpOn lmP5iihH+EJsXVsy9a1QbQwpSH37T5oAgWGhbgh3jEUpHB3nnOvYXsOV57eDFRtdyWKSgrrV1tn/ R4aEiPZVPfqvziAXN05LfQ5IwWl5mejU0YMt2wIor7BfmNat9+mjWlpfG/SHiCZMYsZKJvW0zWB9 qR9ULidzCKgSI4qJ9cEnMRnoZK0tmfpYqNaFXEPKKyibC8hRoW6I5BivKUVCyZ5rjjksDffc2kl5 4bQ2MnNuFTY5wGiBhIj+0N9wuXXrImYApvW2eQur9T1NZuQXbx4nHJ2OO28yf/9hqHqRN/cXX68K dYqPRUHgTxeocYIcb4DGVCm6zi/M3tK8KSFHSL0GTnpYADGvfPFIqTlmZ3ymxcxbrwm5rczUCj79 WiWeea0SVQ6YeqHQ3/+7vwsorLrbElmbjh6RpW++JWG321np+tKm0VJ6qoaBikdLhw5MA0XI/ea7 BvicYYzotscHThUjAtkoRPG6kqk/Noe6lyANz9/USwp5W/MLo/3MohQtMbXXTxzfFpeOU2vSvWRZ LR5+Zifo1e405MQMkH8uFRZFVreN3PzQVGN6uoYNZfavsaxYWY9fNvpwyrFqrTIP6puqm+GvXN2I Tb+xKkXz3CkTo3+bE7peCPnz2pJpxc3btJcgHXDwDacC4tzmF8bymUUpFmrm3pOWJnD1Re2Vxy4i bwsPP70Tv2219w8HucK59rL2utFC1857Pd7mwrU4NxqV0BoLWb+tXe9DXb1963L03SaHrRlpAqr9 4tEot6IyALL+49Q6AVViNOVxMs+Pac0oRKVFw9qSqS81P7HXN7b3oJsmAji2+YWxfmZRipVc/PfR lM+F57bF6UPNd5wYXLsHn6jQg+YFH7P6PcVposXb8We1Qbcuez3WVldHaXmDDkpFwag2ujB9v6YR ZBFnRyKHrZ99XY/ynQEce4TaKdE/Hp6u7xejDbycWiYwcUJbFIw23+MGidGCYlNnPjpflP/gA8XF e8ZH2suoIa9g43JAmCZIBjo2dDBIWPOad2KG8vUicoY7Y16VrdMpZC1HfvdIkJI1vTK7Cq+9UWXr iClnXw/yz2ijIuz1Ht1Khh5Pv7rTEVabe1TMAR9IjGhfndlJgRjpVfT40X/hrJzVwfXd46ckOVSt FVX/BmD6LkkeKQVjV/ueHkrVgfTojyB5bq6qtmfaiJydXj6haR8VbTBN5kR7eMjlEfXEdz82gmIg WZ0qq6Ue/ZnqQF7RVSUa9Xfr6sGOioDt08Oq2hhLvm4TI72NQi5vvkF2D0HKOfjKgwBcHwuQSO5h UYqEUnzXkPHChflqjRfuf2yHvpM/vprGdnf/vim4dFw7XH1RO9CGUjsTTR9R8Luff/FhQ5kfASn1 zZ121YmE4PwxbdDogx5M0w5h+mZVA35c68OQE9QZPPTaLwWnDs7E9h0BikRqF27HlKtKjMizyjtL TJ2m24OZEFi7pmTaouCDe0zZ5Y0tPQ8SLwdfoOL9iNwM3HyV+ebHyR66QtUGOOMZoCm619+2xw9d v14p+nTQaUPUrocZbW3p1fDYTYEEySlp89Q2S9MNSM4bY/7USfOyWvtMDmxnvV1tS+j3lBShj2DH nBbz7pHWmqeff3lWFZ6fWRnRtYl4kSoxeuSZCt3PokpmUorFSwqzhwaXsccIqffASRcBOD74AhXv KaTx5q1+0EY7M1Oyegnvso8Hl09ohzNPVfflnzm3Gv99YaflfugO6OnVR3yTrmwPEiW7Erk9euqV nfr+KtqP01KimE7kk418wdEIhabT7EpHHpKGC85pq6+30FSelYnaTgYPZAlI9VCVaOqW9iuFDXmg qnCb8724oK0+VWt2NawQI6qzEGi/tmTqA8H133OEVFBKw6c9FCv4YrPf80gpfqL0B2/a5H3izyhM DnZEcqV1IfJArvoXdphm66dKvm/QfynG6o2a3DSRqfa4s+wfMVE/vrmgOuTIrjUO8Zy3IuAjjd4L 36pC6WZ7tx3Ewymae0mMzj/H/GfKKjEy2iok9i8qzPl112fjDb3mFWwsBUR28DHV71WJ0pz51Xjs +cQOl2yFJd3N/9ymx8pR/RwY+dOUF5luF4xWN9ozymrt9YH/7sDC98yZQ9+3iwdnjshCwSj72/XQ UxV4u6imteabep6CAF51oZrQB8EVjSYMQvB9bnqfKGKkM5cYubgw512D/64puxMn/NLRG/DcbZyw 6lXV9B1t1mvbRkOihkomx6g3XKHONxv5orvlX9vx68aWp6fMfkbox8kjd3dW7pamtXrTH2zyTk7P plmJPFiv+KYeRR/U6lN5FIPKrkRulAb8IVWfygs3/Whm/Wi9jb6LZHBxxMHqpvDyTsrAth0B/Jig xg6qxOh/L+3EnPnW/kjRny8hvlpbMnW58aztEqQDD/rrURC41Dhh5SuLUnS06aGk0BGqEnldePyF naD1EKsSmS3/5RJ1AhtJO2hK6//u367UcovMo8kwYulHTUYRqr0ctNTunH29GHx8Bsj4wMrNpt+u brJKpLJVJRJcenLJ4i+REhnKXKTAgpbEqPCtaptQiQ1rS6bONQrfJUi9Dp40EsAZxgmrX1mUIiNO bkFUrkfQw0lB9KxMNNq74jz1cZlaahO1+dZ7t+um0i1dY/ZxcpRKI4YPP6unxV0c2Nseg42DD0rV nbeSAYJViUZlbxXVIC1NA4WcUJHIBD4zQ8Pn31jXLhXtMPIkMbpkrPk/Qu0VI2qdrF9bMu0Zo527 BKn3oEkXmOkyyCggmlcWpfC0/nJJO6WL/HdOK8f8peasmYRvye6zfQ9IsSykwe5Sm96RR3ISom+/ t9YCLbgeFPPo4y/qdXGiUOoUE8nqRKO01FSBL1ZaN6Kg+FiffFkP8rWoKs4STYt2bO9ByQ+Nlo72 ze6/xBUjIiXarS2Zep/BbJcg9Rk46RoA/YwTdr2yKO1Nnhb6aUGYFsVVJLJQuu/RHVi52ro/SEY7 aPqxn8Wjg+dmkBCV61M6ofYSGXWz8pWixH74WR2+WtUACoZn9aZf8o9nbFy3st0kghSm5OjD1Kwr 0QisXRtND3hIG2ndllSJ0bPTKzFjrl3TdHv0QlrvQ/76xNpvH9Qrs0uQeg2c9A8Anfe41KYPLEq7 we/X3YsJZ6nzETb7nWrQ2okdsXbItFvFBund9PZ890JhJf7vvnJ9zcQOLwZ71ib0p81b/Hj/kzpQ yIWMdIH9e1jnFql9O49pVoWhWxf6KO2RIiexZDWqItEPnjOGZeplbCxzj1m4KjGiH2SvzrF2Wj5c v2oy8Maakmnr6RpdkJp82FVOBcQugQqXgRXnWJSAwwel4rF7OiubZ6fQ4k+/at8ud/rCDein3tqM /O7d/sB2fP51gy1eC2L5vlD8n/eW12HVj43IzBCwwl8f+Yhb9UOjLXt5SCjeXlyL9FSh7Hknwftt WwA/rbNvijbSZyH/jCxMnGD+uiqJEX0fHJWE9t6akqlfU510Aeox8KoDpMCNjqokoJvdqvDo4AaT 8NzjM3D3X9WFGievC9PftPfBJFdH7dtqyh676W9UYfK0cny8ot62EA3xNo42epJF3k/rfPo2hpxu akdM6ekCxcvNinkTXeuNdSUKRKgqIi15hSfvEbSu5NREYnTlBUkiRmTWAJSsLZm6lPpDf7r98PcF 9nDa4Ji+MmJwmD21Y0QSdeLmWfJQoNJb9z8p6qNNf3SMB8vrgbJf/TPnVYOmIrdud8/0jMGlpdeP Pq8D/acfKqOHZYJc5qhIJx6Trlv92bm29uTLO1G+w6/kjzIxu+L8droFnhN94CWbGFF/aEAf41nW BUlqYn9h3ZYTo+yIX5NJlFQ9kAbsa2/fhlU/WG+8YJRvvHbsYP7sMDkSJQ8dm7YkjhAZvIzX4o9q Qf8piuqo4Vkg7+dmp306emwXc9oXQ/14x40dzW6enh+53UlPE3oIFSUFxJCpqu8+OaB13DRdEB8p sb/xUf+r0HvQjWcC4hTjoBNfk2FNidZUVMwbU3+SJR25VdlQZp73gXieE/pjkD/KHF9cNBp64L8V IH9zZLGVDIlCXsxfUqOviZgdnHDRB7Uod4BFGnkJ+fDzemiK9mmRWTgZcnz6pf17lc4ckYmrLzZ/ Y7grvKELYG3J1Ifoe6uPkITUekp9b7Ozv8qJPFJS5RKEetSJfv0aTJjCp82VJLQ//2JCZs5+9Fus 3btLa7CguAan5WXihsvN/4PWYsEWnfh5XaPuZd7nB+iPttmJ8iTBe+H1Sj3on9n5R5IfRTy+5lLz +84VYtQEKCc3V3qLi4VPHyEdMOCma4RA70jg2X1NIo6UKKYJuc5RkWio/uQr9lnStdQm2ogZT7hl cm1EFkO0sZQTdN9t5GFjZ6XEMYfHt6eHhN4JIySjX/1+6KMYVcYOtFcpI03Tpwgrdlr7PJEYXXtZ UosRdbOW0rby6Z+/nbZTN3ESQvY0Ot8NrzRSomiGZicydCDLLyuT7groTDVi9NQrlfofbSvbw2XZ R4AMEd54txpTHt9hXyUUlkzGDuRdQ0UaNTwTz0ztAnKlZFVSJUZkPetEg41wXGUA+9F5w+a2R7iL nXhu3qIaUOwOs5OVokTid+4Zarwv/PupCpCTVE5MIJEIvPZGFR58wvzvvcHooTv3gdlrckbewa8q xcjOvYXBbYzmfUBqetgjbdSoUpqYNX9yNpraxHgthUhwqyjRnLFheh5j81u87Z8PlevOK1u8gE8w ARcToHUzitOlKt11c0cMO1mN1wiqM4VZUTFNRyMjN4oRMZEIdKNXrTrTY2lAPrMfIpWiRNNpZidy B3PdZe2VLNBSXW+6axve+9iejY1ms+L8mEBLBChkxgXX/qYbtbR0TTzHb/lzB9AoxuykKiCpm8WI GAsNXejVK4Xs7OQ9SJE8ECRKlMz+1WFMpz3xkjmRZ7MyBSaObwearzY7kbXZ7Hersd7CgHpmt4Hz YwLRECjb7Mdjz1cgINVY4NHfE/JcMdMkJ6QsRmF6V4ocOuvVAgFXj5CMJjpdlLp29mD8mWqcpJIY Pf3aTlBUUk5MIF4CNbXWWprFU18yB//PsxXw+SQorpbZieJ0kfd18vsYT1IlRq+/Ve3aabpmPHXH 3l4BdEmUP2NOFaVuXTwYO1qNGJFVFXnr5sQEzCIg3aNHu5r8+Is7dR91tLnc7ERRWj2aiNlyjdw9 me36jNpIYmTW7I3ZzKLPT+xL93ghRQe4fc4uqPVOE6Xe+6XgySlqonok1gMZ1In8lgnEQID2pdXU Slx+nvmRVcnVkNeLqEcjpxybjr9f1yGG1oS/JfG++1KHpEnR9CZ88911lkRJhfUdrSlFY+jQr5c6 MaJFzMT5deSu54tr61wCtNVh2pNqzMJpI3c0338So9tvMN8XX+KJkf486aA0IPEEiZpntyhRBM7H 71MzMiKXIG4173TunzKuWaIQeGdxDf4xpVxJcyL9UapKjMgNWIL+EG0aIQkI8yVcyaMQfaZ2iRIF 1vv3nftEX+EI7qBpCbftwo6gWXwJEzCVAIXquO4favYqkSiF8+hysqKRkRN9UprYaem5F69NJ08N 5jtSMrGW8WZltSgdc1gaptyuRoz+99JOR7uRj7ev+H4mYCaBku8bcOmNW5TsVaJN7aEcotL3/x8K pukSXIyaur06q4NXAubv/jTzqTIhLxIlSqr3KdHDeM+taqK8PvxMhZIvlgl4OQsm4FgCFMLi+cIm /3dm7/8zPIXTd5OSqu9/UogRgFQEMryQSHdosFhTH3LVovTFynplYnT/Yzuw6P1aU3lwZkwgWQiQ B+9HnlWzgdYQueUr6pR8/5NFjOhZDABtvBCIz1e9i55qlaJkeHUwG8fkqeVY9im7AjKbK+eXXATI EzptoA0EpOk+JEmUDGEyk+qbC2rw2PPJs8fQ70EmBehL6DWk5g+IKlFqXo4ZnynC66df2R/N0oy2 cB5MwAkE6A88xVdS9QPSrDaSGJGAJlMSgUAaCVLSjJCMznWDKN04eRu++a7BqDK/MgEmYBIBMpuu b5BQ4dXBjComoxgRN02KtmRll24GRLflQaLk1OHw9XewGLnteeL6uosAbZ+g/XxOS8kqRkY/0Agp aRMtGHo9wJUKwkzECvWKm7diza+Nsd7O9zEBJhAhAdrP1+iTuGSs+a6GIqzCHpeRk+Rkm6YLBiAR aJPUgkQwCt+q1pnYLUr0ML65sBrr1vuC+4jfMwEmoJDAK7Or0NAgbf9RSt9/w3xcYXMdnbUAPCRI zvh5YCMqu0WJHsbpc6uweYvfRgpcNBNITgL0/acwFuG8L6gkw2K0my4JEq0jJX2yS5ToYXx5dhW2 lbMYJf1DyABsI0DT936/NH3zfGsNYjHak1DST9kF47BalOhh5MB6wT3A75mAfQTI0Ims71TELgrV Khajvanw6KgZExIl8hmnOrEYqSbM+TOB6AksKK7FPx9S4yk8uDb0/f/fy+r/zgSX6Yb3JEgujA+p Fq1qUWIxUtt/nDsTiIfAex/XKRUl4/tfV58osbrjob3nvSRITZ4H9zye1J+yMgVy9lU3m0luRkYO yUxqxtx4JuBkAocMUOcvgL7/+WeYH2rdyTwjrZu6v7qR1sBh15EYTRzfTolvquCmUuRJEWR2Hnwu Gd5npFPrOTEB5xG47rL2yr//FBKdEsc2293/EvCzIO3mAavEyCiS9j4JAcyc17QXyjieDK8eXr1M hm52VRszMwQun6D+x6gBhUXJINH0KqBV0Z8FdiUNWC5GRldccX47x/rUMurIr0wg0Ql0bK9ZKkYG TxKliwuSfiuogQM0Qkp6d9JWj4x20f/9DbkuSfEKHr43BxPmcy0vCIehw6eiIdCtiwdjR7dRPk3X Up14pNREJiBkJQlScvk4b/ZU0DDdijWjZsXu9ZEeSq8XePpVtjHZC06IA7SJkRMTiJdA7/1S8OSU zvFmE/f99P0nv3rkyihZk9S0eg0yuUdIVs4Zt/agjTuzDSZO4OF7a5z4PBMwg0D/fs4QI6MtNFPi 1JAYRh1Vvnr8qNEgkncNyQprmmg7kESJLPA4MQG7CGRkJL4F5CEDUvGfu+0fGTXv42QWJQ2o0gSQ lNuFnShGxsNJ0SztcvRo1IFf3UugPs64jhr9VUjgdPxR6Zh2xz6ObWGyilIDtNqkXENyshgZ35Kz R2aB/jAkc3wUgwW/RkegsZHX11oiNuzkDNzy5w4tnXbMcRIlSkm1ppRVvUOTkOodNzmmmwFVYvT6 73GVzGzqmSMy9fqmpyX2L1YzmXFeTKAlAqOHZyoTI4pAa3YiUco/I8vsbJ2aX13x873qvIDY4dQa ml0vVWJ0273b8elXTdbzNN1mZiI3I5QK36pC6WYOUWEmW84reQgUjMoC7flTkcgZK/m/o2SMbMwq xwgcakQiMCtfB+aj65AmZHIIkhVi9MRLO6FipESi9OIjXXFQ3xQHPkdcJSbgbAIXntvGEjGi6bWX Z5lvtk2ilAQjJX2mToOQCT9CskKMjK8kiZIqV0CP/qszjj5MndNHow38ygQShQBto7gwX81Witsf 2D0yMniRbzoWJYNGNK9NAyNNAluiuc1t11opRgabJ1/eielvmv9LifK/99ZOGHJChlEUvzIBJtAC Ado+QdsoVKS/3bMdy1eE9rqmUpTMXhJQwSa2POVmuk8LaFpZbBk4/y47xMigQh4XVPxSovxvu7YD Rg3j8BUGa35lAs0JXHNpe6j64339Hdvw+dfhPa6pEiUSWTLOSMC0ldrkFVLobxKtgXaKkcGSHkpy B2L2Qiflf93E9qANjDPnJp+ncIMvvzKB5gQ6ddBwwTltlfmlu/KWrfh5XWPzYkN+pu8/JcNXXciL Yjh47WXt9bso5HrCJCFLqS3erBp/WU2CzQA5QYyMB4UWOn0+4PLzzJ/HvuK8dsjK0KDC5NSoP78y AbcQ+EOfFDx2jxrvCxTldfa71Vi/0RcVDhIl8lFp9tRhoomSDDQtHWnz5uWQzCaM1DpJjIwnd8bc KvzvJTUOMcj31VUXqjFnNerPr0zA6QTI+4JKMZo+typqMTKY0fS9ijVlEqVEmb4T0DYRLyNM2gYD nptfad7Y2LdjZjuC9xnFmi/tI3jkGTWO1c85PQvU9pQU3kAba//wfe4lcFpeJu66uaOSBtDI6OnX dmLzlvj2AKoUpTOGun9NSRMB3ZZBjxgrpVgvhDxQSY9alCn9QSbPBmanux4q37XpNd68ac63vkHi 5qvMd11CbScXZLPnV2N9aXTTCvG2i+9nAnYRoDhGKqbDqT1vLqjBo89VQJrkickILWP29N31l7fX 16oXFNfa1Q1xlys0/EqZ6CMkoQXWx52jjRmoEiPagf3+7zuwzWoePTQkcioSjQ6fe6gLDh/Ee5VU 8OU8nUWAjIVUidGc+dW6H0mzxMggp2qkRD9yR+S61hjA37ApRx8hNU3ZSbhWkFSKkeEOxHiYzHol kbv5n9vNym6vfKbc3glDT3Ltw7lXe/gAE2hO4OqL2imLHUTeVh57Xs2aL7WDREmFRxcXi1JpcbHQ p3V0QZJC6MOl5p3u9M9uFCOD6Zff1uNPt2zFO4vV2JP87S8dcM5p5vrVM+ruhFdaxD7qEB4JOqEv rKxD504e3eHwGEXP9qtzqkDeVlQnVW7GXClKQQMifQ0JkL8C7loQd7MYGQ/7T+sa8eLrVfD7ocQY 46qL2qFDew3PvGa+J2KjDXa9nnB0Ouh/8Ue1mLuoBt+sijMIkF0NMbFcjwf6jxBVTkRNrGpMWR06 IBVTFcYxou0TVoZ7MITP7A28JEqNPmDJMpesKQUNiHRB8gY8P/pFIKaHxI6bEkGMDG5bt/vx3xeb fpGpsBAcf1YbZKQLPPqc+l99RpusfM09PgP0f+F7tZi3sBrf/RTZpkUr66i6LCEAGjEksvk/rY/Q H1pVibZl2OFRW5Uo3XZNB/h9cpcXclXczMlXrjHy8dCbIwZOqawVlbcCQv9snHTiayKJkcE3EAA+ +bIebdto6N8v1Ths2utBfVPRsb0Hm7b4UbHTGT88AhIgsTQr9TkgBWT+26mjB1u2BVBe4Yx2mtW+ UPmQEJ11ahZ0p7uHmjd9+eaCauxwyHNC7R53Vhtcc0mTd4JQHOI99u+nKvDGu2qmziOp2+ff1CMz Q8OAA8397p9yXAZ+2eDT/0dSD7uuEZDPrymZ9iWVrwvQqlV3yt4DbzofgHPj+gL6XhsVpt3B8Uzs 6hQq97Ov1DyYlDftYj9zRBbW/OrDr1HuNlfBhKYUzj2jjel7pw7snaL7+WvXVmsS4MrEFKbTh2bi v/d2xjGHmydERj/TNHJdvUm2zkamMb6SN5ILzjXvh0vzakyeWo6iD+yf2iJRUvGD1BWipImpa7+d qtsx7BoR9Rk4aSSAfs07zCmfE3FkFIotPZgej8Ah/c39tWSURdNbldUSqx0wtdW/Xwp65vy+jGlU 0KRXGhWSAGdladj0mx87qxJDmIafkoHrr1DnXLeqOoDnZ6rxVB9N12ZmCPzpwvagTd+q0rW3b8OK b8I7SVVVdqh86QdpMoqSP+C9bd2qKfpDt0uQeg2adAyAY0OBsvtYsoiRwfmrkgb9F+qRiqzIjjks TRc9KsfOJCFw8rHpSqswoF+qPq2Vnq5hQ5kP1TXO+OUfbaPph8S1l7bX14q6dNr1tY02m1avX/5F vel771ottNkFBx+UiosK2mLkYPM3ulNR5H2BRkY0neW0pFKUflzr078DDmvzzqWF3f5u1GnXk917 0KT9AZxhnHDKa7KJkcG95IdG/LY1gOOPVvMHm0ZgNK21YmW9aTvRjbpH+kpThxeca77T2VDlD/xD qm6BlpoisHa9zzFTUqHqGnzsxGPS8eeLm+L67Ntl19c1+BJT39Pifumm+NzkxFMhcoMzeVJH9NpP TXRk8r7wyLMVqKl17g8TVaJEcdRoZmSjjf0b4tn4Zm3J1KeM47ue8D4DJmVA4FLjhBNek1WMDPZk Fk4PUJ6iTa40rUW/trduD2BbufVTWrQLnkYsVkbBHXRQKgpGNa1d/bCmEY0ONcr74xFpuPrC9nro gpx91UxrGs+Z8frdj436pk3js9Wv5HlBpck6bUb97wvusDZVJUr0t8RhorR4bcnUN4xnbZcg9Rt4 fVVAaLcYJ+x+/csl7fSpFrPr4RQDhkjbRb9mPvi0DgJCN0yI9L5Ir+vbKwW0QL55qx8/r7N+CoP+ CLZv58FBfdX8Im6JA00LkZUfrddRHWgvmBPSUYem4U8XtsPFBW3RI9saITLa/ez0Sqz5xfpngJwC k+cF+qGgKlGwzKddth+PREnFd8NJoiQhX1tbMu1Do993CdLPqx6q7T3wxisBYc0cilGDEK80RXH2 SPMXM+9/bAeWfBg67HCIajjm0I6KAFaubkCbTE2JKFFDaZMppa9t2GBK0Tc7tPMoa1u4jqSpSwrh QRM4q35oBJng25EOGZCKyye0xcQJ7ZQZeoRr18x51SicZ32wx4EHpuLisW11k/1w9Yvn3JMv78Qr c+w31IilDZ9+mdiipEnt32tWTf3RYLNLkOhArwE3jxQCvY2TdryqEqMpj+/QN0/a0SYzyiQzaZV7 laiOhw5M09eVSJSsHDHQ1N2n9GuwrfUjJaNvDhuYpk+PEWea0rBKmPr3TcGl49rpI4QDelo7SjTa To5En/h9c7ZxzIrXkUOawkb03l9du+/9zw68VWTfHiMzOKoUpa9WNcQdWiOeNgrg1jWrpu6Ky7OH IPUeNOkQAMfHU0A896oUIze7Zg9mSsN4r1fgYEVm4bSuRCOGb75r0PfxBJet+j198eoaJFRZF0ZS /yMObhKm2jqJ739uVGbw0Wf/FN2SjEIH0KZeuxK5ynnyFetdS106ri2uPF9tYMnrbt+m/4izi62Z 5aoSpRG5mfji2wb8ttWWOettiwtzCjVkfQAAIABJREFU/i+Y056CNHBSVwBjgi+w6j2LUeSkv/y2 AVWKjQGGn5Jpy36lku8b8frb1boQqBLdSEjTWs4F57RFTZ3U15giuSeSa2jf1QXntMHNV3cAbeK1 Ky1fUYfHX9iJtxU5922pXZ06aLj8vHYw239bcHlk1v23e7Y70cQ5uJpRv1clSqcOtkeUpBTL166a +mIwiD0Eqc+gSWRz9OfgC6x4z2IUPWVaiCevC7Q/RVWi/UoZGZrlmwdp2oxEd96iGgT8AFnG2ZVI mC48ty12VjWNmGKtR86+Hpx/Tlvcek0HJe6hIq0X+fp7bnqlbk1ntfkvmbBTmHHyGqIqGaEjGhqc a9YdT9tJlMgNmNkM7RAlTcjZa0qmLQrmsYcgHTngwfJaUXUTAHVPTHDppH6KDBhozShRpumaIdv1 kfbxqLTAo4Jo0blrZ3tMw8l9DU0nLHivVl/Tor1EdiVy0UPCtG1HAD+uidxWvMs+TUL09+s7mu6r LFoW0/5XgUeeqcDPNljSnX9OG9D0pMr0YmElnplu/fSjyjaFypvWkhNBlITEY2tWTVsZ3Ma9Yk7k FWxcDghLPDawGAV3RezvyckmsSRHmyoT/TGjMOx2pe7ZXowelqnUnUwkbaNRBnkWX/R+bYtrTB3b a/pGXHIManci56E0NWd29NNI2kWbry/KbwsVPiiDy3/46Qp9RB18LNHfX3dZeyVha668ZSt+Xhf5 j65YOQuBAUUzcr4Lvn9vQRpb+m9IXBd8kYr3LEbmU504vq3uGdn8nHfnSFMihsv83Uetfdeze5Mw qdgaEG1LSJRopEpGEB4N6NhBQ98DUkBTfXYnCjlCnrvtECJqO7mF+scNHZVj+L/7tieM8UK0sFSJ 0hU3b8WaX5WKUsVJA7I7TZ68Z9yjEIK0cRykeC1aMNFcz2IUDa3orqXF4j9doNZ6iUK70wjBbl94 ZKlGMaTI3Qyn3QTI/Y9hGLL7qLXvaGMvTdOpTGS88PLsKmwrt8VCTGXTosrbpaK0aPHMnOHNG7rH GhKd7HPQLTXQpLIREotR8y4w9zNt7ly3wQdyO68qHdDDC7LCa2gEvv3ePgetFPPo4y/q9bAdZApv p/m0KtbR5EuRgW+9dzu+/V7pL9uwVdqvu1f3MpE/Su30MW3kJTdANDJN9kTfQRWb5unH3rJP69XE FhPylbUl04qb991eIyS6YEhB6W8C6NL84ng/sxjFSzDy+7t38+Lc07OUzDEH14KcVU5/swpbttn/ K5W8HdAak0rLw+C2O+X9C4WVeO2Navh89v5xpj9g9GtddbIruqvqdsWTf1amwMTx7ZR835VM3wlt 1OIZ3d5q3ua9Rkh0Qe+BN51EMd2aXxzPZxajeOhFf29lVUCfV8/KND8SZXBtyAcdCd9v2wIgZ7B2 ps1b/Hj/kzqQp3QK206/1hM5kX82GhF9sbLBMs8SoXimpQndKSo5R1Wd/jGlPOGtZ2NhSE6CVY6U Fr5Xq+99jKVuoe5Jlbj+p1VT97KQakGQbuwJiGGhMorlGDlOHHOa+UP4ZDDtjoV38D3kJ67BB5AH ApXp+KPS9TDMX5XUg8KT25nKNvtRvLwOZA1Hgd5UBQG0q43kXeH2B7brU5VWungK1V4ajf7v/s6g uFMqE60X3fVQue7WSWU5bs5bpSileIW+FGBSPLHvFhbmTAnFOqQg9Rn0Vz8gJ4a6IdpjtMCuIuoj mbLOX2J/6OFoedhx/berG/S9J4MVbqKldg04MFXf/PnLRp8jgp9RXJ+lH9Xhp3U+PRJnTjd3j5im v1GFO6eVY/mKekeEzZg4oa3ug0/1M134VjUee36na4MrquYTnD+JEq0jZ2WY64iZNuKmmidKs9eW TN1ruo7aEVKQ9usyZZOWVTUJQFw/e0iMVLgIof0wtIufU+QE1pf68M6SWqSlqgljEVyTU47NAE3j kLcFJyRqe9EHtfi11I8O7TRYEejOzHbTAv49/9mBDz6pc0RgwWOPSMNVF7XHqbnqrRtpi8FLr7vT U7eZz0A0eTU0Sn00QwJipkcHs0RJSPlQ8w2xRvtCCtK6dXcG+gyYdAoE+hgXRvuqUozs3JwZLQcn XU9RMmmXN8WfoXhAKtOgP6Tqng0oTLRTQkWvW+/TvT5s2uJHp44ePTihSgbx5j3r7Wrc/98KFH9U 65jRAe11u3Zie0tiNd3yr+1Y/AHPgsTyHNHUGlnbOlGU/H55/brvpoWMlBhSkAjAAQMn7S8EhsQC g8UoFmrW3UMjF4oSe9xRasKjB7eEzM8zMzR8/Z29C+/BdSLXOfOX1FjGILjsSN6/8W41pjxeoY/q yDjFCYnWCGktmLxDq07U/kl3bQeNbDnFTkC1KJERRdQRlwXWLH29+90ttapFQeo98AYfhLispRtb Os5i1BIZZx3/cW2jbpGmKhJtcGv1taUxbWyLShtcl+D3xOClWbSxMoDjjlQvzsFlh3pPcXumPlGh W5FV7HSGENH+LgoaSBGcaSuB6vTcjErQfiq7jTVUt9Oq/FWKEu19ilqUBApbWj8iJi0K0lEDp5bV iqprAUT8TWUxsuoxM6ecHTubTMPT0zRY4biUotKSb7MNZT49tIU5rYg/F3KWSsK0s1KCnKhancik lox0SJBos69T0ojcDDx+b2dLng1qM5l0v2NxOAynsFZZDxKlX0t9GGNyFG5aU4pWlATEA2tKppa0 1N6QG2ONi/MKSmcDONv4HO6VxSgcHeefOy0vEzdeoX5To0Hi6dcqQVZjTkvkqJb2VV2p2P0StZvW huYuqsE3NoSND8edfAWeOTxTubNeow40RUcjI/rDyUkdgd77peDJKZ1NL4BM8p9+LSIryIBX+rss KOy5vaVKtDhCohv6DLipEwROb+lm4ziLkUHCva80fbXkwzp4NHMtc1oiQvuiaCqPDC2ctlZAZrOv zKlCQ4Oa/VsffV6nmzFPf7Pa1vDRofrmgnPb4I4bO4IiB1uR6IfJs9Mro1+LsKJyCVYGjb7JFRB5 1DAzGSOllasbQLHMwqTPFxX2eCTMeYQdIeXmr+/rEZ4fw2XAYhSOjjvP0Y57CmNuVZozv1p3Bkqe FpyWPB6A9m+dPjQzbstEmpKjX5M//2KvR4tQjIeckKH/oVJtfWmU7RQHvUZ9kulV5Ujp4WcqwqG8 Z/HMPUOWN784rCDRxXkFpd8DOLD5jfSZxSgUlcQ4RtE9J09SHzogmBYFWCPvzQHnLKMEVw8d2ms4 8uA0HH5wKg7okYIe2R60ydL2uMb4QKbltFb2/U+NesTdVT80wOc8vdVDZYwengmasrUq0b6qJ18O afVrVRWSvhw7RElInFRUmLMsHPxIBOlBALRJdo/EYrQHjoT8QPuVaDf+OQrcPoUDZncgwHB1a36O XBNlpGtITxfw+yRq6qTugbqx0dnrISSk487MwrgzrRsJE7up/yMPK7ypvflzZMfnfr1S8Ph9ataU QoyUtneS2V0LC0XYn2Vh15AIUu+BN9H8wkXBwFiMgmkk7nsaqZAvvPKdARx7RMTGlnED+eMR6Ti4 f6rulYCC3zk50Zw5hUCg/UJVNRL1DdKxIzziqGnAhLPb4P7/64RBijdHB/cbGS7c8eAOfPOdM7x3 BNctWd9v3xHAF9824NTB5o6OaU2JQqzTJvygNOutwnazgj6HfNuqIHU64cGN6XW7zb9ZjEJyTOiD P/zciIXv18LjESDv3lak7K5e5B6Xgb69UnQT8dLNYX9YWVElV5dB1oMU4v4/d3fG4YOsNW0n9z/P z6zi2EUOfIJ+2+q3RJSkEHevLZm6qjUErU7ZUQZDC0pnSiCfpm9UDPEp0Nbsd6pbqyufdwABKyLS hmqmU02kQ9XVScdIiGh96IbLrTPpN9pPBhxvLqwGuWzi5GwCNFr+9537mF5Jipf2n2d3NAqZ0aWo sFNYiwcqPCJByhu7cdzE8e1eUyFGHGzL9GdAeYb79/DirBHqg/+FasiyT+t0x7orvtljOiDUpUl9 jIRoxCmZuOkq64WIwD/1SiVmzHXePrOkfihaabwqUVr6Ue36ISdk7tdK8frpiASp9LfGU7O7eOdH kmE017AYRUPLeddSWJGrLmxnS8U+/6ZeN6H+8LM6W8p3aqFkpj5qWJbu6seOOvKoyA7q5pV5+KBU TLnd/JESgH5CiJ9aq2lEgiSlfA3AuNYyi+Y8i1E0tJx7bbcuHj3ECK1P2JHW/NqoC9O7xbVwumWb Sj5tszR9H9Gl49RHbW2pHbRW9PpbPPXeEh+3HD/msDTcc2sns6vbTQixubVMIxWkCwG80FpmkZ5n MYqUlHuus9r1UCgy09+swluLakB7gJIl0X4S2nk/api5llLR8CMLOgqi58SNzdG0g6/dTcBkUSoW QgzenXvL7yIVpA4AylvOJvIzLEaRs3LblakpAhcXtEXBaHtGSwavT76ox4L3an6PrOrs/UBGnaN5 peCHucel47QhmZY5Pm2pfryvqCUy7j9uoihdI4R4NBIiEQkSZSSlfBPA6EgybekaFqOWyCTW8aMO SdN/tZN3b7sTWfkUfVCD1T81QrpYm8hI4ZD+qcg7McNSrwot9R+NRskHnVO9arRUbz4eHQGTRClb CLEpkpKjEaSxAKZHkmmoa1iMQlFJ7GMFo7Jwxfn2GD2EIkvrGxQGfNWPDa4QJxKhAf1SccIx6SCW TkhLljV5KP92NW9wdUJ/WFEH+mF5500xuxFbJIQYHmk9oxEkmqSOacWSxSjS7ki869q20XDe2W10 wwcnta7og1p8+mU9Vqysh1OC4RGfrEyBwwam4Y9HpOlTck5i9uhzO0HrRZySj8Apx6bj9htiEqVL hBDPR0osYkGiDKWULwK4INLM6ToWo2hoJe61hw5IxejhWTjlOPun8ZpTXrfBh5XfNej/v1/TiNJN PstGUB3bayBXKxQgkTwoWOUJozmDcJ9fnVOFFwo5ims4RslwLkZRai+EiNiTbrSCdCqAiPcjsRgl w2MaXRuHnZyBW/5MNjLOTuRzjTwMbNzkQ+kmP0o3+0ARdmMZTZGT2k4dNHTu6EGPHA96ZnuxXw+v 7mm7a+dWvXfZBmreoqZwGWRaz4kJEIEoRalQCFEQDbloBYm+PRH5AWExiqYbkutaWhspGNUGl59n 356ZWIn7/UBFZUDf80SOVBt9TZYSXo8Atcv4nJmu6Y5MMzMFaI+QmxJZKc5dVA165cQEmhOIQpRO F0K80/z+cJ+jEiTKSEp5L4C/hcuUxSgcHT5nEKCRw4Sz2oCilHJyBoEHn6jAu0s5PIQzesO5tYhw psMrRPhwE81bGIsg/QHA6uYZGZ/Zh5VBgl8jJdC+rYb8M7Iw7iwWpkiZmX0dGSy8uaDasrUzs+vP +VlPYERuBm6+qsXp9/uEELdGW6uoBYkKkFJ+AODE5oU9N6MSr8xmh4rNufDnyAjQesroYZksTJHh MuUqms14/W0WIlNgJmEmYUTpICEERRuPKsUqSJcCeCa4JBajYBr8Ph4CZHlGUWp5xBQPxfD3khDN nl8NWhPjxATiIRBClJYJIU6KJc9YBYl26e0aCrEYxYKe72mNAIXZPuvUTN0dUWvX8vnICDz8dAXe XlzDHhYiw8VXRUigmSidJ4R4NcJb97gsJkGiHKSU1wJ4+LHnd2LOfN4stwdV/mAqAa9X4PS8DFxz qT2xfUxtjA2Zfb2qAXMXVuP9j+t4jcgG/slSZJssbdurj+57aVaWmBtrm2MWJCrw7oe3n7j0wzpa T+LEBJQTILPqIw9Jw+jhmTj+KOdtsFUOIMoCFhTXYt6iat2PX5S38uVMIGoCAuL+opnZYS2wW8s0 LkGizPPyS5dCILe1gvg8EzCTABlAnDE0ExPOZsu85lzJ6ek7S2qwoyLQ/BR/ZgKqCASE39OvaNa+ a+IpwARBKjsXQhbGUwm+lwnESsDrAY45PB3DT8nAicck76jpvY/r9FhQX5XU87RcrA8T3xczAQEx t2hm9pkxZ/D7jXELUm6u9Hq6lv0MIKKY6fFWmO9nAi0RoP1Mg0/I0GMFDTootaXLEub4N6sasPjD Wn1tqLKKR0MJ07EubIgATi2ambMg3qrHLUhUgSEFZTcJyCnxVobvZwJmEdinowcn/TEdJx+brscR Mitfu/MhH3sffV6H95bXYcs2ttm2uz+4fJ3AqsUzswcBIu6IY6YIUu5Zazt4UtPWA+AJfX5CHUeA zMePPCQVfzw8HUcflgba5+SmRNNxH6+ow8df1INHQm7queSoqwCuKJqZ85QZrTVFkKgiQwrKHhGQ 15hRKc6DCagk0LO7F4f2T8XB/VP10VOXfZzjcZsct67+sRG0FvTFtw1Y84u7I92q7EfO2xEEttZI /37LC3vWmlEb0wRpeP6mXn4R+AGA14yKcR5MwCoC7dtpOLB3CvockIJ+vVLQM8eD7vt6kZZm2tcj ZFPKNvvx60Yfftnow/c/NYBiMW36jafhQsLig84kIMUdiwuz7zKrcqZ+4/IKSl8GcJ5ZleN8mICd BMhIonMnD9q1FaBpP4p+2yZT6EKV4hWgDbspv//8CgRoszjg+X2wVVsndW8IdQ0S9fUSOysD+nTb zqqAbo69dbsfPtYeO7uXy46fQJVX+vdfUNhze/xZNeVg7mjG438Afs8EAKYKnVmN5XyYQDQEaPqM /nNiAkxgbwISeMpMMaISTF3dXfxaz29EFBFl924iH2ECTIAJMAEXEGjwavIhs+tpqiBR5aSG+8yu JOfHBJgAE2ACziEgIF5ZOL07WVabmkwXpMXTcz4A5BJTa8mZMQEmwASYgFMI+ODX7lZRGdMFiSop Ie5UUVnOkwkwASbABGwmIOTL8fqsa6kFSgRpycyc9wEsbalQPs4EmAATYAKuJODzBwL/UlVzJYJE lZXAZFWV5nyZABNgAkzAFgKvFBf2/ElVycoE6fdR0iJVFed8mQATYAJMwFICDR6pKV2OUSZIhEnT cFvTYMlSaFwYE2ACTIAJmE5APrWwsNta07MNylCpIC2anvM5gFlB5fFbJsAEmAATcB+BGq8vVdna kYFDqSBRIR4/bgfgMwrkVybABJgAE3AXASnwyILZXcpU11q5IC2clbMaAi+obgjnzwSYABNgAkoI bGvwND6gJOdmmSoXJCrP25hCo6TqZmXzRybABJgAE3A4ASlw97JX9y+3opqWCBIN9SQER5S1oke5 DCbABJiAWQQEfqxon/2YWdm1lo8lgkSVyKqlEOeitLUK8XkmwASYABNwBgEZwC0rnhSNVtXGMkGa Ny+nRorA361qGJfDBJgAE2ACsROQwHtLCnPmxJ5D9HdaJkhUtZP757wACTIF58QEmAATYALOJeD3 yMD1VlfP8kB6eWPLjoaUH5sdi8lqcFweE2ACTCBRCUghH18yo/vVVrfP0hESNW7xjOzPADxrdUO5 PCbABJgAE2idgAS2BOobyMuO5clyQaIWpkpBjd1heWu5QCbABJgAEwhPQOC24jd62fL32RZBml+Y vQVC/F94KnyWCTABJsAErCQgpPzk5P7Zts1g2SJIBPik/t2eAPS1JCt5c1lMgAkwASYQmoDPr8kr J08WgdCn1R+13KghuEmDx244VJMaWd15g4/zeybABJgAE7CYgJBTFs/o/leLS92jONtGSFSLpTN6 fA2IaXvUiD8wASbABJiA1QTWeVI9SmMdRdIgWwWJKphZKwmC0hgbkYDga5gAE2ACyUpACnH1wpe6 2e5v1HZBIg8Omha4nAP5JetXgdvNBJiAzQReXjIje77NddCLt12QqBaLpvdYDOBJJwDhOjABJsAE kojAJq/0X+eU9jpCkAhGXV3KzQB+dQoYrgcTYAJMINEJSImrFxT23O6UdjpGkD6c26VSCslTd055 MrgeTIAJJDgBMd1q56mtAXWMIFFFl8zovhCQj7dWaT7PBJgAE2AC8RAQpV7p+3M8Oai411GCRA30 pHnIDv4HFY3lPJkAE2ACTABSQF7qpKk6o08cJ0hkehjQtAsB+IxK8isTYAJMgAmYRUD+t2hmzgKz cjMzH8cJEjVu6fRun0DIe8xsKOfFBJgAE0h2AlJgdWatsNUbQ7g+cKQgUYX9m3P+CYmPwlWezzEB JsAEmEDEBBoAnEd7PyO+w+ILHStIxcXCJ/2YIIByi5lwcUyACTCBxCMg8dclM3K+cHLDHCtIBG3J 7JxfpBRXOBkg140JMAEm4AICby8uzH7E6fV0tCARvMWF2a8L4Amng+T6MQEmwAScSUCU+j24GBDS mfXbXSvHCxJVtVr6b4TEl7urze+YABNgAkwgAgI+aHJc8Ws5WyO41vZLXCFIywt71krNkw+gwnZi XAEmwASYgEsISIhbF0/P+cAl1YUrBIlgLpmx789CyIvYK7hbHi2uJxNgAjYTmLNkZrepNtchquJd I0jUqqIZ3d+EkA9G1UK+mAkwASaQbAQEfhQy/RI3rBsFd42rBIkq3imQc6uUgsJVcGICTIAJMIG9 CVSLgHZOUWEn1y1xuE6QCguFPw0YD4Ff9u4HPsIEmAATSGoCUgAXFRV2W+lGCq4TJII8vzB7SwCB MwE4dsexGx8GrjMTYALuJiCBfxXNzJnl1la4UpAI9tIZPb6WQlzMRg5uffS43kyACZhKQMq3Th6Q fYepeVqcmbC4PNOLyysoux2Qd5meMWfIBJgAE3APgZVCpp/kxnWjYMSuFyRAiryCspfIaWBww/g9 E2ACTCAZCEhgS8AXOKJ4do8Nbm+va6fsdoMXspMsvxSQH+8+xu+YABNgAklBoEZq2qhEECPqrQQY ITU9dCPzy7o0CnwkIfsmxWPIjWQCTCDZCUgJed6Smd1fSxQQCTBCauoKsrzzSd9IGr4mSudwO5gA E2ACLREQErclkhhROxNmhGR02uBxm/6oBQJLAGQax/iVCTABJpBIBKSQjy+Z0f3qRGoTtSVhRkhG x1D484CU4ynorHGMX5kAE2ACCUTglX0COdckUHt2NSXhBIlatrSw+1wJ/IX3KO3qZ37DBJhAQhCQ S8iIizzWJERzmjUi4absgts3NL/0b1Lg3uBj/J4JMAEm4FICnwqZPtzte43CsU/IEZLR4KLCnPsE xP3GZ35lAkyACbiUwKpUKc5IZDGifknoEVLTg6dvnH0cwJUufRC52kyACSQzAYFf/I2BExNlr1G4 rkzoEVJTw2njbPafAbwSDgSfYwJMgAk4kMAGf8A/NBnEiNgnwQip6RHLz5eebaL0VQFR4MCHjqvE BJgAE2hGQJZByNzFM3r80OxEwn5MGkGiHjzyCpnSvrxsuhAYk7A9yg1jAkzA9QRog78mcErRjJzv XN+YKBqQBFN2u2mseFI07oPy8QJi7u6j/I4JMAEm4BwCuhhJLS/ZxIh6IKkEiRpcWDiwoaPcni8l ZjvnEeSaMAEmwAR0ApulRwx2a8TXePswqabsgmHl5kqvZ99NL0PKscHH+T0TYAJMwB4CotTjl3kL Z+Wstqd8+0tNuhGSgby4WPg6BbpRDKWXjWP8ygSYABOwicB6v/SdksxiRNyTVpCo8eR+o5PMpjDo /7PpIeRimQATSHICAuInvyZOLi7s+VOSo0ges+/wHS3FkLFl9wiJv4W/js8yASbABEwkIMU3fnhG FBd23WRirq7NKqlHSLt7TcglM3JuFVL8lR2y7qbC75gAE1BIQOKj+pSGXBaj3YyT1qhhN4I93w0Z WzpRSDwBwLPnGf7EBJgAEzCNwLuZtThn3rycGtNyTICMWJBCdOKQgo2jBASFBc4KcZoPMQEmwARi JiAgnivv0O1K2hcZcyYJeiMLUgsdS5FnRSAwTwBdWriEDzMBJsAEoiQg/7l4Zs4dgJBR3pgUl7Mg henm3Pz1fb3CO19C9g1zGZ9iAkyACbRGwC+Aq4pm5jzV2oXJfJ4FqZXezx1f2tnjxxwAJ7ZyKZ9m AkyACYQiUCGAsUUzcxaEOsnHdhNgK7vdLEK+K34tZ2tqZfVQ3kAbEg8fZAJMIDyBtQGPOIHFKDwk 4yyPkAwSEbzmFZTeCuBfyRS2IwIsfAkTYAIhCYjlnkZx1sI53X4LeZoP7kWABWkvJOEPDMkvPVsI vAigTfgr+SwTYAJJS0DI51N31vxp/vx+9UnLIIaGsyDFAG342E2DAlLOYWOHGODxLUwgsQn4hJA3 Fs3o/p/Ebqaa1rEgxcj1xAm/dEzzpbwK4NQYs+DbmAATSCACehwjTRQUTc8uTqBmWdoUNmqIEfey V/cv7ySzzwBwL7sbihEi38YEEoWAxOfw4WgWo/g6lEdI8fHT7x6aX3a6FPIFAPuYkB1nwQSYgIsI CCEeTdlZdROvF8XfaSxI8TPUcxiev6mXH4GZEDjKpCw5GybABJxNoAoCf1o8I+cVZ1fTPbXjKTuT +mphYbe1qVXVJ9KvJZ7CMwkqZ8MEnEtgpcePo1mMzO0gHiGZy1PPbejYjWdKKZ7hKTwFcDlLJmAz ASnk44GMhhuLn+9VZ3NVEq54FiRFXZo7ZkMPzau9LIBTFBXB2TIBJmAtge1SYuKSwhxyJcZJAQGe slMAlbIsnt1jwz4yOw8Q/wAku5lXxJmzZQIWEVgqJA5nMVJLm0dIavnquQ8bV3pUIACywhtgQXFc BBNgAuYRqBPAbScOyH548mQRMC9bzikUARakUFQUHDsuf31Gpua5FxLXsi88BYA5SyZgNgGJLwFx /uLC7FVmZ835hSbAghSai7KjeQVlgwXw5P+3d26xVVRRGP7/PbW2oqYgSIGC8a6QoPLgJYKppYL1 Fi+p0AcUrw/6oDHxHhU1XuOLiYmJqVGjRvAIGjERQ9GCGNEgmGgUlYhSORRRqQhKy5lZZho1EUs9 9zMz5386ObPXWnutb03yZ86Zvbe2HSoZYgUWgUIJDBj48K8NjQ/rVNdCUebmL0HKjVdRrAeflliz ALBbANQUJaiCiIAIFEzAgA/DPS8CAAAGbElEQVRovF5PRQWjzCuABCkvbMVxap275RQL2AlgWnEi KooIiECeBHYCvGvG5Man9V9RngSL4CZBKgLEQkI0N1uNO7z3ZsIeAFBfSCz5ioAI5EOAS/2Mf0P4 Zmw+3vIpHgEJUvFYFhSp9bJtRwUueIa0mQUFkrMIiEC2BLbB7KYVqQmLsnWQXWkJSJBKyzfH6MaW Oen5ND4OYHSOzjIXARHIjoABfK7GMre+k5r4S3YusioHAQlSOSjnOMfs9p5RGboHAV6vlx5yhCdz ERiewDrS3di1qHHN8GYarQQBCVIlqGc55+BLDz6fBDEjSxeZiYAIDE3gJxrunj5lXKdeWhgaUBSu SpCi0IVhczC2XJ6eS/BRAJOGNdWgCIjAvgQGQDzl9/c/2P3GkX37Dup7tAhIkKLVj/1mE65dGgHv JiNuB9CwX0MNiIAI/E3gdd/827pTEzf+fUGf0SYgQYp2f/6TXXNHerTn2/0ArgN4wH8MdEEEqpwA zT6C5+7QceLxuxEkSPHr2WDGze09x3j0FgDoAKBd22PaR6VdVAJfkLina9G41wFaUSMrWFkISJDK grl0k8zs6JmKjHsI5AWlm0WRRSDCBIjvYbxvlDW+lErRj3CmSu1/CEiQ/gdQXIZb5/SebkFwL4i2 uOSsPEWgQAKbaXx0JH55NpWaMlBgLLlHgIAEKQJNKGYKLe1bTyXtXgDnFzOuYolAhAh8R8MjI7Hj eQlRhLpShFQkSEWAGMUQ4aGAvo87SVys/5ii2CHllAeBrwh7fEfD+Bd1LEQe9GLgIkGKQZMKSfHs 9i3HO8dbYZgHoLaQWPIVgQoR+JjEY9NPHPeGFrVWqANlmlaCVCbQlZ7mnLmbxwfm3QzjdVrHVOlu aP4sCAQElhn4xIpXx72Xhb1MEkBAgpSAJuZSwqx5vSP8Absy3OUYwHG5+MpWBMpAYDdgL4D25IpF TV+XYT5NESECEqQINaOcqSxYYG7lF73nerAbDZgNwCvn/JpLBP5FgPjGjM8EA3s6tcXPv8hU1RcJ UlW1e+hiW9vTkwLyKsKu1n55QzPS1ZIQ6AfwGh07uxY2rtRi1pIwjlVQCVKs2lXaZMOnplVf9s5G YNeSdqG2Jiot7yqO/hkMz9bAf1HnEVXxXTBE6RKkIaDoEjCzY9tY8zPzSV4Dw7FiIgIFEtgF8FWY 37ki1fRhgbHknlACEqSENrZ4ZYXHX2ydQeIKGi41YGTxYitSwgkEAFbC7OXa2rrU2y8ftjPh9aq8 AglIkAoEWE3ubW3fHDhwyIg2g3UQvBBAfTXVr1qzJGBYC+AV52UWLl84KZ2ll8xEABIk3QR5ETjz ou2H1B04cB7Jyww4D8CIvALJKQkEDLCPaG4JAre4a/HYb5NQlGooPwEJUvmZJ27G8PDAg+iFr45f YkAbgTGJK1IF7UsgA9gqI5cGe4PXupc0/bCvgb6LQK4EJEi5EpP9sATa28372UufxoAXwHg+aFOH ddBgnAj8BGAZzN4i6pd1pUb9GqfklWv0CUiQot+jWGfYfOkPTTU1nGVw5xhspp6eYtXO8EiHNQSW +84tH+2PXavzhmLVv9glK0GKXcvim3C4zmn1hvRJ8F2rITgL5HTtqxepfvowrIfDKgZ8NwNvZXfq 8F2RylDJJJqABCnR7Y12ceHPezu89FTzeRaIGQBOBzAh2lknKrvdMKwDsdrI9/v/qFn9wZtjfktU hSomVgQkSLFqV/KTbWnvmeCcdxrMzjDwVAAnAzg0+ZWXvMKMARsIfGLAGs+CNXu3T/i8u5uZks+s CUQgSwISpCxByaxSBIwtc348CgimATaNNihQk7Xn3rD92EmzL438FMT6gO7Tg3cHny1dOv73Yb00 KAIVJiBBqnADNH1+BMJ1ULUH+ZO9wJ9ixAk0d7yZHQ3iaAB1+UWNlZcBCF+13mjARho2mLPPXcAN Xanxm2NViZIVgb8ISJB0KySMgHHW3HSTH7hjBp+iDEcYrYmGpsHvxMSY/AQY7oS9zYAewLYQDD83 G7DJkRsz9f2bup8/ck/CmqdyqpyABKnKb4BqLL95/qa62j21Y3xzjQDHGvwxNDfKYA2ObDCzBpg1 GFy9ozWY8QA4Oxhm9QT/efraz75+vxMIxQTh9gUg+mDYS2CXGcKxPwJaH8k+GnfArC8g+hyw3Rx+ 9PZie+DqtmqNTzXemar5T7boKrYfCqI6AAAAAElFTkSuQmCC"/></symbol><symbol viewBox="0 0 24 24" id="webpack" xmlns="http://www.w3.org/2000/svg"><path d="M19.376 15.988l-7.709 4.45-7.708-4.45V7.087l7.708-4.45 7.709 4.45z" fill="#fff" fill-opacity=".785" stroke-width="0"/><path d="M12.286 1.98c-.21 0-.41.059-.57.179l-7.9 4.44c-.32.17-.53.5-.53.88v9c0 .38.21.711.53.881l7.9 4.44c.16.12.36.18.57.18.21 0 .41-.06.57-.18l7.9-4.44c.32-.17.53-.5.53-.88v-9c0-.38-.21-.712-.53-.882l-7.9-4.44a.945.945 0 0 0-.57-.179zm0 2.15l7 3.939v2.104h-.016v5.177h.016v.54l-7 3.939-7-3.94V8.07l7-3.94zm0 2.08l-4.9 2.83 4.9 2.83 4.9-2.83-4.9-2.83zm-5 5.08v3.58l4 2.308v-3.58l-4-2.308zm10 0l-4 2.308v3.58l4-2.308v-3.58z" fill="#8ed6fb"/><path d="M12.286 6.21l-4.9 2.83 4.9 2.83 4.9-2.83-4.9-2.83zm-5 5.08v3.58l4 2.308v-3.58l-4-2.308zm10 0l-4 2.308v3.58l4-2.308v-3.58z" fill="#1c78c0"/></symbol><symbol viewBox="0 0 24 24" id="wolframlanguage" xmlns="http://www.w3.org/2000/svg"><title>wolframLanguage</title><g transform="scale(.12121)" fill="none" fill-rule="evenodd"><circle cx="99.197" cy="98.946" r="83.28" fill="#212121" stroke-width=".841"/><path d="M182.529 98.828a83.406 83.406 0 0 1-39.14 70.721.064.064 0 0 1-.038.019l-28.62-35.665 23.71 2.612s11.385 1.177 13.978 0c2.373-.938 15.175-18.963 15.175-18.963s-36.75-23.23-49.312-36.032c1.434-21.575-1.656-50.269-1.656-50.03-9.251 9.234-10.429 10.669-19.68 19.203-4.028-13.04-5.923-17.547-9.95-30.588-12.104 9.95-21.337 26.799-27.977 46.48a78.68 78.68 0 0 0-4.23 5.094 109.774 109.774 0 0 0-2.667 3.66 114.558 114.558 0 0 0-5.132 8.002 172.555 172.555 0 0 0-3.403 6.051c-7.706 14.475-14.034 31.066-19.515 46.001a.858.858 0 0 1-.092-.184c-14.988-30.912-9.502-67.85 13.822-93.072 23.325-25.223 59.723-33.575 91.71-21.045 31.988 12.53 53.029 43.382 53.017 77.736z" fill="#e53935"/><path d="M101.452 69.178s-1.416-8.295-2.373-11.367c6.401-6.18 7.357-7.118 13.52-13.04.477 11.845.238 18.006-.479 32.481-3.55-3.568-10.668-8.074-10.668-8.074zm-27.737 40.778s-6.64-4.029-11.624-4.728c1.435-3.329 5.223-7.596 6.18-8.773-1.913.699-15.653 6.86-17.087 12.084a74.804 74.804 0 0 1 11.385 3.79 35.993 35.993 0 0 0-8.774 20.158s21.815-3.33 38.185-1.196c.283.168.609.251.938.24l8.534.239 27.111 45.136.221.35c-.037.018-.055.037-.073.037-51.133 18.485-88.085-15.543-95.976-27.443.034-.102.058-.206.074-.313 7.1-30.017 15.855-65.939 30-76.552 7.356-12.82 9.49-31.783 22.751-41.734 3.33 9.951 8.553 30.588 12.103 40.539 15.653 15.652 39.361 35.094 55.234 43.15 1.656.956 3.79 7.596 3.79 7.596l-6.401 8.056-68.276-6.879a54.462 54.462 0 0 0-4.58-.183 86.848 86.848 0 0 0-14.144 1.36c3.311-8.295 10.43-14.935 10.43-14.935zm22.054-8.774c3.789-.46 7.817.956 12.323 3.568 4.267-1.195 4.745-1.434 9.013-2.612-5.463-4.028-11.386-8.295-19.442-7.118a47.249 47.249 0 0 0-1.894 6.162z" fill="#fff" stroke-width=".936"/></g></symbol><symbol viewBox="0 0 24 24" id="word" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m7 1.5V9h5.5L13 3.5M7 13l1.5 7h2l1.5-3 1.5 3h2l1.5-7h1v-2h-4v2h1l-.9 4.2L13 15h-2l-1.1 2.2L9 13h1v-2H6v2h1z" fill="#01579b"/></symbol><symbol viewBox="0 0 24 24" id="xaml" xmlns="http://www.w3.org/2000/svg"><path d="M18.93 12l-3.47 6H8.54l-3.47-6 3.47-6h6.92l3.47 6m4.84 0l-4.04 7L18 18l3.46-6L18 6l1.73-1 4.04 7M.23 12l4.04-7L6 6l-3.46 6L6 18l-1.73 1-4.04-7z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="xml" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m.12 13.5l3.74 3.74 1.42-1.41-2.33-2.33 2.33-2.33-1.42-1.41-3.74 3.74m11.16 0l-3.74-3.74-1.42 1.41 2.33 2.33-2.33 2.33 1.42 1.41 3.74-3.74z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 24 24" id="yaml" xmlns="http://www.w3.org/2000/svg"><path d="M5 3h2v2H5v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5h2v2H5c-1.07-.27-2-.9-2-2v-4a2 2 0 0 0-2-2H0v-2h1a2 2 0 0 0 2-2V5a2 2 0 0 1 2-2m14 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3h2m-7 12a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m-4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="yang" xmlns="http://www.w3.org/2000/svg"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2m0 2a8 8 0 0 0-8 8 8 8 0 0 0 8 8 4 4 0 0 1-4-4 4 4 0 0 1 4-4 4 4 0 0 0 4-4 4 4 0 0 0-4-4m0 2.5A1.5 1.5 0 0 1 13.5 8 1.5 1.5 0 0 1 12 9.5 1.5 1.5 0 0 1 10.5 8 1.5 1.5 0 0 1 12 6.5m0 8a1.5 1.5 0 0 0-1.5 1.5 1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5 1.5 1.5 0 0 0-1.5-1.5z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 289.99999 290.00001" id="yarn" xmlns="http://www.w3.org/2000/svg"><path d="M250.733 218.418c-12.39 2.943-18.661 5.653-33.993 15.641-24.004 15.487-50.176 22.688-50.176 22.688s-2.168 3.252-8.44 4.723c-10.84 2.633-51.647 4.878-55.364 4.956-9.988.077-16.105-2.555-17.809-6.66-5.188-12.388 7.434-17.809 7.434-17.809s-2.788-1.703-4.414-3.252c-1.471-1.47-3.02-4.413-3.484-3.33-1.936 4.724-2.943 16.261-8.13 21.45-7.125 7.2-20.598 4.8-28.573.619-8.75-4.646.62-15.564.62-15.564s-4.724 2.788-8.518-2.942c-3.407-5.266-6.582-14.248-5.73-25.32 1.084-12.777 15.176-25.011 15.176-25.011s-2.477-18.661 5.653-37.787c7.356-17.422 27.179-31.437 27.179-31.437s-16.648-18.352-10.454-35c4.027-10.84 5.653-10.763 6.97-11.227 4.645-1.781 9.136-3.717 12.466-7.356 16.648-17.964 37.864-14.557 37.864-14.557s9.911-30.431 19.203-24.469c2.865 1.859 13.163 24.778 13.163 24.778s10.996-6.426 12.235-4.026c6.659 12.931 7.433 37.632 4.49 52.654-4.955 24.778-17.344 38.096-22.3 46.459-1.161 1.936 13.319 8.053 22.456 33.373 8.44 23.152.929 42.587 2.245 44.756.232.387.31.542.31.542s9.679.774 29.114-11.228c10.376-6.427 22.688-13.628 36.703-13.783 13.55-.232 14.247 15.719 4.104 18.12z" fill="#2c8ebb" stroke-width=".774"/></symbol><symbol viewBox="0 0 24 24" id="zip" xmlns="http://www.w3.org/2000/svg"><path d="M14 17h-2v-2h-2v-2h2v2h2m0-6h-2v2h2v2h-2v-2h-2V9h2V7h-2V5h2v2h2m5-4H5c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#afb42b"/></symbol></svg>
\ No newline at end of file +<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 24 24" id="actionscript" xmlns="http://www.w3.org/2000/svg"><text style="line-height:113.99999857%" x="5.605" y="15.892" transform="scale(.91325 1.095)" font-weight="400" font-size="42.822" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#f44336"/><path style="line-height:125%" d="M4.744 2.031c-1.157 0-1.994.31-2.51.93-.515.612-.771 1.678-.771 3.197v2.467c0 1.408-.402 2.111-1.201 2.111v2.035c.8 0 1.2.679 1.2 2.036v2.654c0 1.512.26 2.562.78 3.152.52.59 1.355.885 2.502.885V19.43c-.447 0-.77-.151-.97-.453-.195-.303-.292-.815-.292-1.538v-2.267c0-1.807-.404-2.937-1.214-3.395v-.045c.81-.464 1.214-1.581 1.214-3.351V6.025c0-1.283.42-1.925 1.262-1.925V2.03zm14.66 0V4.1c.842 0 1.262.642 1.262 1.925v2.268c0 1.843.402 2.996 1.207 3.46v.046c-.805.442-1.207 1.544-1.207 3.306v2.356c0 .715-.099 1.22-.299 1.516-.2.302-.52.453-.963.453v2.068c1.152 0 1.984-.295 2.494-.885.516-.59.772-1.663.772-3.218V14.84c0-1.379.404-2.069 1.209-2.069v-2.035c-.805 0-1.21-.696-1.21-2.09V6.113c0-1.49-.255-2.54-.77-3.152-.516-.62-1.348-.93-2.495-.93zm-3.054 4.46c-.455 0-.886.057-1.293.173a3.056 3.056 0 0 0-1.078.527c-.308.241-.551.549-.731.924-.18.37-.27.817-.27 1.336 0 .663.165 1.227.493 1.695.33.468.831.864 1.502 1.188.263.125.509.249.736.37.227.12.422.244.586.374.168.13.299.271.394.424a.963.963 0 0 1 .145.521c0 .144-.03.28-.09.405a.9.9 0 0 1-.275.318c-.12.088-.272.158-.455.21a2.34 2.34 0 0 1-.635.075c-.415 0-.825-.083-1.233-.25a3.644 3.644 0 0 1-1.13-.763v2.222a3.68 3.68 0 0 0 1.101.418c.427.093.875.139 1.346.139.459 0 .894-.05 1.305-.152a3.002 3.002 0 0 0 1.09-.5c.31-.237.556-.543.736-.918.183-.38.275-.849.275-1.405 0-.403-.052-.755-.156-1.056a2.542 2.542 0 0 0-.45-.813 3.295 3.295 0 0 0-.704-.633 6.754 6.754 0 0 0-.922-.535 12.4 12.4 0 0 1-.676-.348c-.2-.115-.37-.231-.51-.347a1.502 1.502 0 0 1-.322-.375.91.91 0 0 1-.115-.453c0-.153.033-.288.101-.408a.948.948 0 0 1 .29-.32c.123-.089.275-.156.454-.202a2.18 2.18 0 0 1 .598-.078c.16 0 .326.015.502.043.18.028.36.07.539.13.18.056.354.13.522.218.171.088.329.188.472.304V6.871a4.039 4.039 0 0 0-.957-.285 6.448 6.448 0 0 0-1.185-.096zm-8.774.165l-3.123 9.967h2.094l.605-2.217h3.053l.61 2.217h2.107L9.869 6.656H7.576zm1.072 1.78h.047c.028.347.077.646.145.896l.922 3.35H7.564l.934-3.377c.08-.288.13-.578.15-.87z" font-weight="400" font-size="51.019" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="android" xmlns="http://www.w3.org/2000/svg"><path d="M15 5h-1V4h1m-5 1H9V4h1m5.53-1.84L16.84.85c.19-.19.19-.51 0-.71a.513.513 0 0 0-.71 0l-1.48 1.48C13.85 1.23 12.95 1 12 1c-.96 0-1.86.23-2.66.63L7.85.14a.501.501 0 0 0-.7 0c-.2.2-.2.52 0 .71l1.31 1.31C6.97 3.26 6 5 6 7h12c0-2-1-3.75-2.47-4.84M20.5 8A1.5 1.5 0 0 0 19 9.5v7a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 20.5 8m-17 0A1.5 1.5 0 0 0 2 9.5v7A1.5 1.5 0 0 0 3.5 18 1.5 1.5 0 0 0 5 16.5v-7A1.5 1.5 0 0 0 3.5 8M6 18a1 1 0 0 0 1 1h1v3.5A1.5 1.5 0 0 0 9.5 24a1.5 1.5 0 0 0 1.5-1.5V19h2v3.5a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5V19h1a1 1 0 0 0 1-1V8H6v10z" fill="#c0ca33"/></symbol><symbol viewBox="0 0 24 24" id="angular" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#e53935"/></symbol><symbol viewBox="0 0 24 24" id="angular-component" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#0288d1"/></symbol><symbol viewBox="0 0 24 24" id="angular-directive" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#ab47bc"/></symbol><symbol viewBox="0 0 24 24" id="angular-guard" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#43a047"/></symbol><symbol viewBox="0 0 24 24" id="angular-pipe" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#00897b"/></symbol><symbol viewBox="0 0 24 24" id="angular-resolver" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#43a047"/></symbol><symbol viewBox="0 0 24 24" id="angular-routing" xmlns="http://www.w3.org/2000/svg"><path d="M11 10H5L3 8l2-2h6V3l1-1 1 1v1h6l2 2-2 2h-6v2h6l2 2-2 2h-6v6a2 2 0 0 1 2 2H9a2 2 0 0 1 2-2V10z" fill="#43a047"/></symbol><symbol viewBox="0 0 24 24" id="angular-service" xmlns="http://www.w3.org/2000/svg"><path d="M12.102 2.625l8.84 3.15-1.34 11.7-7.5 4.15-7.5-4.15-1.34-11.7 8.84-3.15m0 2.1l-5.53 12.4h2.06l1.11-2.78h4.7l1.11 2.78h2.05l-5.5-12.4m1.62 7.9h-3.23l1.61-3.87z" fill="#ffca28"/></symbol><symbol viewBox="0 0 100 100" id="apiblueprint" xmlns="http://www.w3.org/2000/svg"><title>api-blueprint</title><path d="M50.133 7.521A16.998 16.998 0 0 0 33.135 24.52a16.998 16.998 0 0 0 4.945 11.974L24.861 57.398a16.998 16.998 0 0 0-3.175-.308A16.998 16.998 0 0 0 4.688 74.088a16.998 16.998 0 0 0 16.998 16.998 16.998 16.998 0 0 0 16.998-16.998 16.998 16.998 0 0 0-7.063-13.773l12.576-19.89a16.998 16.998 0 0 0 5.936 1.093 16.998 16.998 0 0 0 6.154-1.155l12.537 19.83a16.998 16.998 0 0 0-7.244 13.895 16.998 16.998 0 0 0 16.998 17 16.998 16.998 0 0 0 16.998-17A16.998 16.998 0 0 0 78.578 57.09a16.998 16.998 0 0 0-2.95.262L62.337 36.327A16.998 16.998 0 0 0 67.13 24.52 16.998 16.998 0 0 0 50.132 7.522z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="applescript" xmlns="http://www.w3.org/2000/svg"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" fill="#78909c"/></symbol><symbol viewBox="0 0 24 24" id="appveyor" xmlns="http://www.w3.org/2000/svg"><path d="M12 2c-.084 0-.165.008-.248.01a10 10 0 0 0-.266.01 9.952 9.952 0 0 0-.754.066 10 10 0 0 0-.148.018 9.855 9.855 0 0 0-.93.177 10 10 0 0 0-.07.02c-.196.049-.392.1-.584.16v.012a10 10 0 0 0-2 .875V3.34c-.02.012-.038.027-.059.039a10 10 0 0 0-.953.635c-.09.067-.172.142-.26.213a10 10 0 0 0-.628.546c-.109.104-.211.211-.315.319a10 10 0 0 0-.476.539c-.1.12-.201.237-.295.361a10 10 0 0 0-.52.766c-.088.143-.17.288-.252.435a10 10 0 0 0-.363.723c-.072.161-.136.327-.2.492a10 10 0 0 0-.269.778c-.02.067-.044.131-.062.199a10 10 0 0 0-.008.027c-.098.364-.166.728-.22 1.09-.012.077-.024.153-.034.23a9.85 9.85 0 0 0-.08 1.182c0 .03-.006.057-.006.086a10 10 0 0 0 .008.148c.001.094-.002.188.002.282l.011.004a10 10 0 0 0 .333 2.158l-.012-.004c.012.047.033.091.047.139a10 10 0 0 0 .322.955c.02.052.037.106.059.158a10 10 0 0 0 .503 1.035c.065.116.14.226.21.34a10 10 0 0 0 .423.64c.092.128.187.252.285.375a10 10 0 0 0 .448.52c.112.123.222.248.341.365a10 10 0 0 0 .803.719 10 10 0 0 0 .01.006c.099.078.207.146.309.22a10 10 0 0 0 .648.442c.138.085.28.163.424.242a10 10 0 0 0 .715.358c.114.051.226.106.343.154a10 10 0 0 0 1.133.389c.016.004.031.01.047.015a10 10 0 0 0 .461.098 10 10 0 0 0 .482.103 10 10 0 0 0 .418.051 10 10 0 0 0 .575.065 10 10 0 0 0 .144.005A10 10 0 0 0 12 22a10 10 0 0 0 .197-.01 10 10 0 0 0 .496-.025 10 10 0 0 0 .49-.043 10 10 0 0 0 .489-.074 10 10 0 0 0 .51-.098 10 10 0 0 0 .47-.12 10 10 0 0 0 .477-.14 10 10 0 0 0 .47-.172 10 10 0 0 0 .481-.197 10 10 0 0 0 .414-.201 10 10 0 0 0 .475-.252 10 10 0 0 0 .39-.238 10 10 0 0 0 .452-.301 10 10 0 0 0 .38-.291 10 10 0 0 0 .385-.315 10 10 0 0 0 .375-.347 10 10 0 0 0 .36-.363 10 10 0 0 0 .293-.334 10 10 0 0 0 .353-.434 10 10 0 0 0 .28-.393 10 10 0 0 0 .263-.4 10 10 0 0 0 .264-.461 10 10 0 0 0 .228-.436 10 10 0 0 0 .195-.437 10 10 0 0 0 .196-.48 10 10 0 0 0 .228-.69 10 10 0 0 0 .028-.094 10 10 0 0 0 .021-.066 10 10 0 0 0 .098-.461 10 10 0 0 0 .103-.482 10 10 0 0 0 .051-.418 10 10 0 0 0 .065-.575 10 10 0 0 0 .005-.144A10 10 0 0 0 22 12a10 10 0 0 0-.01-.197 10 10 0 0 0-.025-.496 10 10 0 0 0-.043-.49 10 10 0 0 0-.074-.489 10 10 0 0 0-.098-.51 10 10 0 0 0-.12-.47 10 10 0 0 0-.14-.477 10 10 0 0 0-.172-.47 10 10 0 0 0-.197-.481 10 10 0 0 0-.201-.414 10 10 0 0 0-.252-.475 10 10 0 0 0-.238-.39 10 10 0 0 0-.301-.452 10 10 0 0 0-.291-.38 10 10 0 0 0-.315-.385 10 10 0 0 0-.347-.375 10 10 0 0 0-.363-.36 10 10 0 0 0-.334-.293 10 10 0 0 0-.434-.353 10 10 0 0 0-.393-.28 10 10 0 0 0-.4-.263 10 10 0 0 0-.461-.264 10 10 0 0 0-.436-.228 10 10 0 0 0-.437-.196 10 10 0 0 0-.48-.195 10 10 0 0 0-.69-.228 10 10 0 0 0-.094-.028 10 10 0 0 0-.066-.021 10 10 0 0 0-.461-.098 10 10 0 0 0-.482-.103 10 10 0 0 0-.418-.051 10 10 0 0 0-.575-.065 10 10 0 0 0-.144-.005A10 10 0 0 0 12 2zm-.016 5.002a5 5 0 0 1 .262.01 5 5 0 0 1 .227.011 5 5 0 0 1 .341.05 5 5 0 0 1 .135.019 5 5 0 0 1 .014.004 5 5 0 0 1 .115.025 5 5 0 0 1 .303.076 5 5 0 0 1 .265.086 5 5 0 0 1 .2.074 5 5 0 0 1 .242.106 5 5 0 0 1 .228.11 5 5 0 0 1 .196.109 5 5 0 0 1 .244.15 5 5 0 0 1 .17.12 5 5 0 0 1 .224.171 5 5 0 0 1 .186.16 5 5 0 0 1 .176.164 5 5 0 0 1 .172.18 5 5 0 0 1 .177.203 5 5 0 0 1 .133.172 5 5 0 0 1 .16.223 5 5 0 0 1 .133.214 5 5 0 0 1 .12.21 5 5 0 0 1 .107.216 5 5 0 0 1 .109.24 5 5 0 0 1 .084.223 5 5 0 0 1 .08.242 5 5 0 0 1 .07.264 5 5 0 0 1 .047.207 5 5 0 0 1 .045.277 5 5 0 0 1 .028.227 5 5 0 0 1 .02.351 5 5 0 0 1 .003.079 5 5 0 0 1-.012.271 5 5 0 0 1-.011.227 5 5 0 0 1-.05.341 5 5 0 0 1-.019.135 5 5 0 0 1-.004.014 5 5 0 0 1-.025.115 5 5 0 0 1-.076.303 5 5 0 0 1-.086.265 5 5 0 0 1-.074.2 5 5 0 0 1-.106.242 5 5 0 0 1-.11.228 5 5 0 0 1-.109.196 5 5 0 0 1-.15.244 5 5 0 0 1-.12.17 5 5 0 0 1-.171.224 5 5 0 0 1-.16.186 5 5 0 0 1-.164.176 5 5 0 0 1-.18.172 5 5 0 0 1-.203.177l-.002.002c-.018.019-.028.035-.047.053l-3.959 5.09-3.05-.979a141.684 141.684 0 0 0 3.177-3.084 5 5 0 0 1-.103-.015 5 5 0 0 1-.149-.024 5 5 0 0 1-.115-.025 5 5 0 0 1-3.57-3.04 5.072 5.072 0 0 1-.206-.661 5 5 0 0 1-.033-.147c-.025-.118-.036-.24-.054-.36-.987.993-1.964 1.993-2.954 3.05l-.98-3.053 5.092-3.957c.043-.044.082-.07.125-.11a5 5 0 0 1 .71-.634c.18-.13.367-.25.561-.356a5 5 0 0 1 .16-.08 4.94 4.94 0 0 1 .516-.222 5 5 0 0 1 .147-.057c.211-.07.43-.123.654-.164a5 5 0 0 1 .172-.027c.236-.035.476-.058.722-.059zM12 9a3 3 0 0 0-.053.002 3 3 0 0 0-.166.01 3 3 0 0 0-.133.011 3 3 0 0 0-.17.026 3 3 0 0 0-.113.021 3 3 0 0 0-.19.05 3 3 0 0 0-.103.03 3 3 0 0 0-.16.057 3 3 0 0 0-.129.053 3 3 0 0 0-.146.072 3 3 0 0 0-.12.063 3 3 0 0 0-.132.082 3 3 0 0 0-.123.08 3 3 0 0 0-.116.088 3 3 0 0 0-.126.105 3 3 0 0 0-.1.094 3 3 0 0 0-.111.111 3 3 0 0 0-.096.107 3 3 0 0 0-.094.116 3 3 0 0 0-.098.136 3 3 0 0 0-.072.11 3 3 0 0 0-.076.133 3 3 0 0 0-.07.132 3 3 0 0 0-.063.14 3 3 0 0 0-.054.14 3 3 0 0 0-.077.228 3 3 0 0 0-.007.026 3 3 0 0 0-.03.138 3 3 0 0 0-.031.149 3 3 0 0 0-.014.11 3 3 0 0 0-.02.183 3 3 0 0 0-.001.052A3 3 0 0 0 9 12a3 3 0 0 0 .002.053 3 3 0 0 0 .01.166 3 3 0 0 0 .011.133 3 3 0 0 0 .026.17 3 3 0 0 0 .021.113 3 3 0 0 0 .05.19 3 3 0 0 0 .03.103 3 3 0 0 0 .057.16 3 3 0 0 0 .053.129 3 3 0 0 0 .072.146 3 3 0 0 0 .063.12 3 3 0 0 0 .082.132 3 3 0 0 0 .08.123 3 3 0 0 0 .088.116 3 3 0 0 0 .105.126 3 3 0 0 0 .094.1 3 3 0 0 0 .111.111 3 3 0 0 0 .107.096 3 3 0 0 0 .116.094 3 3 0 0 0 .136.098 3 3 0 0 0 .11.072 3 3 0 0 0 .133.076 3 3 0 0 0 .132.07 3 3 0 0 0 .135.06 3 3 0 0 0 .153.061 3 3 0 0 0 .216.07 3 3 0 0 0 .004.003 3 3 0 0 0 .026.007 3 3 0 0 0 .138.03 3 3 0 0 0 .149.031 3 3 0 0 0 .11.014 3 3 0 0 0 .183.02 3 3 0 0 0 .011.001 3 3 0 0 0 .041 0A3 3 0 0 0 12 15a3 3 0 0 0 .053-.002 3 3 0 0 0 .166-.01 3 3 0 0 0 .133-.011 3 3 0 0 0 .17-.026 3 3 0 0 0 .113-.021 3 3 0 0 0 .19-.05 3 3 0 0 0 .103-.03 3 3 0 0 0 .16-.057 3 3 0 0 0 .129-.053 3 3 0 0 0 .146-.072 3 3 0 0 0 .12-.063 3 3 0 0 0 .132-.082 3 3 0 0 0 .123-.08 3 3 0 0 0 .116-.088 3 3 0 0 0 .126-.105 3 3 0 0 0 .1-.094 3 3 0 0 0 .111-.111 3 3 0 0 0 .096-.107 3 3 0 0 0 .094-.116 3 3 0 0 0 .098-.136 3 3 0 0 0 .072-.11 3 3 0 0 0 .076-.133 3 3 0 0 0 .07-.132 3 3 0 0 0 .06-.135 3 3 0 0 0 .061-.153 3 3 0 0 0 .07-.216 3 3 0 0 0 .003-.004 3 3 0 0 0 .007-.026 3 3 0 0 0 .03-.138 3 3 0 0 0 .031-.149 3 3 0 0 0 .002-.008 3 3 0 0 0 .012-.101 3 3 0 0 0 .02-.184 3 3 0 0 0 .001-.011 3 3 0 0 0 0-.041A3 3 0 0 0 15 12a3 3 0 0 0-.002-.053 3 3 0 0 0-.01-.166 3 3 0 0 0-.011-.133 3 3 0 0 0-.026-.17 3 3 0 0 0-.021-.113 3 3 0 0 0-.05-.19 3 3 0 0 0-.03-.103 3 3 0 0 0-.057-.16 3 3 0 0 0-.053-.129 3 3 0 0 0-.072-.146 3 3 0 0 0-.063-.12 3 3 0 0 0-.082-.132 3 3 0 0 0-.08-.123 3 3 0 0 0-.088-.116 3 3 0 0 0-.105-.126 3 3 0 0 0-.094-.1 3 3 0 0 0-.111-.111 3 3 0 0 0-.107-.096 3 3 0 0 0-.116-.094 3 3 0 0 0-.136-.098 3 3 0 0 0-.11-.072 3 3 0 0 0-.133-.076 3 3 0 0 0-.132-.07 3 3 0 0 0-.14-.063 3 3 0 0 0-.14-.054 3 3 0 0 0-.228-.077 3 3 0 0 0-.026-.007 3 3 0 0 0-.138-.03 3 3 0 0 0-.149-.031 3 3 0 0 0-.008-.002 3 3 0 0 0-.101-.012 3 3 0 0 0-.184-.02 3 3 0 0 0-.011-.001 3 3 0 0 0-.041 0A3 3 0 0 0 12 9z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 720 720" id="arduino" xmlns="http://www.w3.org/2000/svg"><defs><symbol id="ana" preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg"><path fill="none" stroke-opacity="100%" stroke-width="60" stroke="#00979c" d="M174 30a10.5 10.1 0 0 0 0 280C364 320 344 30 544 30a10.5 10.1 0 0 1 0 280C354 320 374 30 174 30"/><path d="M528 205v-32.8h-32.5v-13.7H528V126h13.9v32.5h32.5v13.7h-32.5V205H528z" text-anchor="middle" fill="#00979c" stroke-width="20" stroke="#00979c" font-family="sans-serif" font-size="167"/><path fill="#00979c" stroke="#00979c" stroke-width="23.6" transform="matrix(1.56 0 0 .64 -366 .528)" d="M321 266v-17.4h53.3V266H321z"/></symbol></defs><title>Layer 1</title><use x="20.063" y="360.85" transform="matrix(.997 0 0 .997 -18.596 -159.19)" xlink:href="#ana"/></symbol><symbol viewBox="0 0 24 24" id="assembly" xmlns="http://www.w3.org/2000/svg"><path d="M1.746 1.566v20.905H5.13v-2.088H3.438V3.656h1.69v-2.09H1.747zm17.219 0v2.09h1.693v16.727h-1.693v2.09h3.383V1.566h-3.383zM15.196 3.988c-.5 0-.93.076-1.29.225-.359.15-.652.372-.877.671-.226.302-.39.673-.494 1.108a6.715 6.715 0 0 0-.155 1.54c0 .573.049 1.083.15 1.528.1.442.264.811.49 1.11.222.298.512.524.872.676.36.153.795.23 1.304.23.518 0 .954-.075 1.308-.224.353-.153.643-.376.869-.671.219-.29.38-.661.484-1.112.104-.454.156-.967.156-1.54 0-.573-.052-1.079-.152-1.515a2.92 2.92 0 0 0-.485-1.106 2.09 2.09 0 0 0-.868-.686c-.354-.155-.79-.234-1.312-.234zm-6.814.12a.941.941 0 0 1-.138.458.849.849 0 0 1-.356.296A1.71 1.71 0 0 1 7.385 5a5.244 5.244 0 0 1-.631.037v1.11H8.19v3.6H6.754v1.188h4.545V9.745H9.894V4.11H8.382zm6.814 1.138c.375 0 .643.176.805.527.161.348.241.933.241 1.756 0 .814-.082 1.399-.247 1.756-.164.356-.43.534-.799.534-.369 0-.636-.178-.8-.534-.165-.357-.248-.941-.248-1.749 0-.829.082-1.415.243-1.763.162-.35.43-.527.805-.527zm-6.33 7.64c-.5 0-.93.073-1.29.223-.359.15-.651.374-.877.673-.225.302-.39.67-.494 1.106a6.715 6.715 0 0 0-.155 1.54c0 .573.05 1.082.15 1.527.1.442.264.814.49 1.112.222.3.514.525.874.677.36.152.793.229 1.302.229.519 0 .954-.076 1.308-.225.354-.153.643-.376.869-.672.22-.29.38-.66.484-1.111.104-.455.156-.967.156-1.54 0-.573-.05-1.079-.15-1.515a2.923 2.923 0 0 0-.487-1.106 2.084 2.084 0 0 0-.867-.686c-.353-.156-.791-.232-1.313-.232zm5.846.119a.941.941 0 0 1-.138.457.85.85 0 0 1-.356.296 1.71 1.71 0 0 1-.503.137 5.245 5.245 0 0 1-.631.037v1.112h1.435v3.597h-1.435v1.189h4.545v-1.189h-1.405v-5.636h-1.512zm-5.846 1.137c.375 0 .643.176.805.527.162.347.241.933.241 1.756 0 .813-.08 1.399-.245 1.755-.164.357-.432.534-.8.534-.37 0-.637-.177-.802-.534-.164-.356-.245-.939-.245-1.746 0-.83.08-1.418.242-1.765.161-.35.43-.527.804-.527z" fill="#ff6e40"/></symbol><symbol viewBox="0 0 24 24" id="aurelia" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="api" x1="-31.824" x2="19.682" y1="-11.741" y2="35.548" gradientTransform="scale(.95818 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#apa"/><linearGradient id="apa" x1="-3.881" x2="2.377" y1="-1.442" y2="4.304"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apj" x1="12.022" x2="-15.716" y1="13.922" y2="-23.952" gradientTransform="scale(.96226 1.0392)" gradientUnits="userSpaceOnUse" xlink:href="#apb"/><linearGradient id="apb" x1=".729" x2="-.971" y1=".844" y2="-1.477"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="apk" x1="-23.39" x2="23.931" y1="-57.289" y2="8.573" gradientTransform="scale(1.0429 .95884)" gradientUnits="userSpaceOnUse" xlink:href="#apc"/><linearGradient id="apc" x1="-2.839" x2="2.875" y1="-6.936" y2="1.017"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apl" x1="-53.331" x2="6.771" y1="-30.517" y2="18.785" gradientTransform="scale(.99898 1.001)" gradientUnits="userSpaceOnUse" xlink:href="#apd"/><linearGradient id="apd" x1="-8.212" x2="1.02" y1="-4.691" y2="2.882"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apm" x1="-14.029" x2="41.998" y1="-23.111" y2="26.259" gradientTransform="scale(1.0003 .99965)" gradientUnits="userSpaceOnUse" xlink:href="#ape"/><linearGradient id="ape" x1="-1.404" x2="4.19" y1="-2.309" y2="2.62"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="apn" x1="31.177" x2="3.37" y1="41.442" y2="3.402" gradientTransform="scale(.96254 1.0389)" gradientUnits="userSpaceOnUse" xlink:href="#apf"/><linearGradient id="apf" x1="1.911" x2=".204" y1="2.539" y2=".204"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="apo" x1="-31.905" x2="19.599" y1="-14.258" y2="42.767" gradientTransform="scale(.95823 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#apg"/><linearGradient id="apg" x1="-3.881" x2="2.377" y1="-1.738" y2="5.19"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="app" x1="4.301" x2="34.534" y1="34.41" y2="4.514" gradientTransform="scale(1.002 .99796)" gradientUnits="userSpaceOnUse" xlink:href="#aph"/><linearGradient id="aph" x1=".112" x2=".901" y1=".897" y2=".116"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".53"/><stop stop-color="#CD0F7E" offset=".79"/><stop stop-color="#ED2C89" offset="1"/></linearGradient></defs><g transform="rotate(11.282 -1.694 21.569) scale(.47102)" clip-rule="evenodd" fill="none" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414"><path d="M8.002 6.127L4.117 8.719.116 2.723 4 .13z" transform="rotate(-11.284 17.839 -78.732)" fill="url(#api)"/><path d="M9.179 1.887l6.637 9.946-7.906 5.276-6.637-9.946L.115 5.43 8.02.153z" transform="rotate(-11.284 129.49 -99.884)" fill="url(#apj)"/><path d="M7.3 1.88l1.462 2.189-6.018 4.015L.124 4.16l1.315-.877L6.143.144z" transform="rotate(-11.284 167.2 -62.32)" fill="url(#apk)"/><path d="M2.328 1.146L4.016.02l2.619 3.925L2.75 6.537 1.29 4.347l2.197-1.466zm-1.04 3.201L.132 2.612l2.197-1.466 1.158 1.735z" transform="rotate(-11.284 104.37 -149.22)" fill="url(#apl)"/><path d="M5.346 9.155l-1.315.877L.03 4.035 6.047.019l2.805 4.204L4.15 7.36l4.703-3.138 1.197 1.793z" transform="rotate(-11.284 81.819 7.645)" fill="url(#apm)"/><path d="M14.533 9.934l1.197 1.793-7.907 5.276-1.196-1.793L.052 5.358 7.958.082z" transform="rotate(-11.284 17.141 -7.825)" fill="url(#apn)"/><path d="M6.235 7.177L4.038 8.643 2.84 6.849.036 2.646 3.92.053 7.923 6.05z" transform="rotate(-11.284 18.188 -79.174)" fill="url(#apo)"/><path d="M18.955 35.925L17.48 34.45l3.998-3.998 1.475 1.475z" fill="#714896"/><path d="M33.33 21.55l-1.475-1.474 1.867-1.868 1.475 1.475z" fill="#6f4795"/><path d="M7.12 24.09l-1.525-1.525 3.998-3.998 1.525 1.525z" fill="#88519f"/><path d="M21.495 9.714L19.97 8.19l1.868-1.868 1.524 1.525z" fill="#85509e"/><path d="M31.418 23.462l-6.72 6.72-1.475-1.474 6.72-6.721z" fill="#8d166a"/><path d="M18.058 10.101l1.525 1.525-6.721 6.72-1.525-1.524z" fill="#a70d6f"/><path d="M2.375 11.769l1.9 1.9-1.9 1.901-1.901-1.9z" fill="#9e61ad"/><path d="M15.523 36.482l1.9 1.9-1.9 1.901-1.9-1.9z" fill="#8053a3"/><path d="M8.372 38.294L.017 29.876 29.749.08l8.636 8.201z" transform="translate(1.823 1.548)" fill="url(#app)"/></g></symbol><symbol viewBox="0 0 24 24" id="autohotkey" xmlns="http://www.w3.org/2000/svg"><path d="M5 3c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5zm3.668 3.447a.9.9 0 0 1 .652.256.84.84 0 0 1 .262.625c0 .34-.014.852-.041 1.537-.022.68-.033 1.19-.033 1.53 0 .111-.016.326-.047.644a6.149 6.149 0 0 0-.033.68l2.578-.485c1.007-.179 1.874-.281 2.603-.308.018-.3.048-1.105.088-2.416.01-.345.115-.742.317-1.19.25-.55.533-.826.851-.826.237 0 .448.08.631.236.197.17.295.382.295.637a.775.775 0 0 1-.025.201c-.09.327-.135.612-.135.854 0 .125-.014.32-.041.584-.023.26-.033.453-.033.578 0 .425-.022 1.056-.067 1.893a38.963 38.963 0 0 0-.068 1.892c0 .327.025.816.074 1.465.05.649.074 1.136.074 1.463a.84.84 0 0 1-.261.625.893.893 0 0 1-.65.254 1 1 0 0 1-.686-.254.777.777 0 0 1-.29-.611c0-.327-.015-.818-.046-1.471a39.552 39.552 0 0 1-.041-1.47c0-.256.004-.482.013-.679-.702.032-1.57.142-2.603.33-.86.157-1.719.316-2.578.477-.01.304-.042.812-.096 1.523a22.354 22.354 0 0 0-.066 1.538.84.84 0 0 1-.262.625.893.893 0 0 1-.65.253.898.898 0 0 1-.653-.253.84.84 0 0 1-.262-.625c0-.452.038-1.128.114-2.028.08-.9.12-1.575.12-2.027 0-.573.015-1.436.042-2.586.027-1.155.04-2.017.04-2.59a.84.84 0 0 1 .263-.625.895.895 0 0 1 .65-.256z" fill="#4caf50"/></symbol><symbol viewBox="0 0 24 24" id="autoit" xmlns="http://www.w3.org/2000/svg"><defs id="ardefs8"><style id="arstyle4482">.cls-1{fill:#5d83ac}.cls-2{fill:#f0f0f0;fill-rule:evenodd}</style><style id="arstyle4510">.cls-1{fill:#5d83ac}.cls-2{fill:#f0f0f0;fill-rule:evenodd}</style></defs><g id="arg4522" transform="translate(-59.538 -26.404) scale(.0555)"><path d="M12.8 2.133A10.666 10.666 0 0 0 2.136 12.799 10.666 10.666 0 0 0 12.8 23.465a10.666 10.666 0 0 0 10.668-10.666A10.666 10.666 0 0 0 12.8 2.133zm.15 4.713c.456 0 .836.105 1.142.314.306.21.565.469.78.78l6.089 8.812H9.627l1.82-2.506h3.36c.315 0 .589.01.822.03a11.93 11.93 0 0 1-.473-.663 39.13 39.13 0 0 0-.517-.75l-1.748-2.578-4.577 6.467H4.746l6.25-8.813c.204-.281.46-.534.772-.757.31-.224.705-.336 1.181-.336z" transform="matrix(16.89188 0 0 16.89188 1072.761 475.745)" id="arcircle4514" fill="#1976d2" stroke-width=".026"/></g></symbol><symbol viewBox="0 0 213.33333 213.33333" id="babel" xmlns="http://www.w3.org/2000/svg"><path d="M50.22 199.659c-.875-.406-1.261-1.6-.857-2.652.404-1.053.12-1.914-.63-1.914s-1.615.748-1.92 1.663c-.328.983-1.27.302-2.304-1.667-.962-1.831-3.718-5.533-6.126-8.226-9.418-10.535-7.71-27.444 5.432-53.77 12.459-24.96 23.117-39.033 45.966-60.696 30.229-28.66 52.679-46.223 70.587-55.22 10.98-5.518 13.025-5.059 2.778.624-11.004 6.102-11.378 6.359-10.512 7.226.33.33 7.306-2.67 15.504-6.667 15.87-7.737 16.34-7.912 16.34-6.082 0 .652-4.95 3.738-11 6.858-13.062 6.736-12.722 6.48-10.472 7.872 1.117.69 5.428-.582 11.54-3.406 5.367-2.48 10.397-4.508 11.179-4.508 2.755 0-3.928 5.302-11.541 9.157-20.437 10.35-68.937 46.043-68.07 50.097.166.777-5.792 7.639-13.241 15.248-15.257 15.587-26.14 30.002-33.748 44.706-6.379 12.326-7.457 17.734-5.385 26.996 3.482 15.56 11.592 18.366 31.482 10.895 28.228-10.603 45.758-28.704 47.022-48.556.602-9.442-1.317-13.479-8.52-17.93-4.01-2.48-5.268-2.621-12.065-1.365-4.173.771-10.153 2.906-13.289 4.744s-6.455 3.34-7.377 3.34c-.922 0-3.216 1.336-5.096 2.968-1.88 1.633.48-1.13 5.247-6.14 6.82-7.167 7.956-8.9 5.333-8.132-5.208 1.525-10.194 4.33-15.649 8.803-2.76 2.264-.923.175 4.08-4.641 11.565-11.131 21.183-15.97 33.088-16.641 17.097-.966 27.254 5.805 31.964 21.31 2.435 8.017 2.609 10.24 1.353 17.37-1.65 9.361-7.034 21.553-15.593 35.307-4.398 7.067-8.434 11.427-15.588 16.844-9.166 6.94-15.654 11.02-15.654 9.845 0-.295 2.455-2.161 5.455-4.147 8.818-5.835 5.075-5.377-8.326 1.02-6.854 3.27-15.199 6.593-18.542 7.38-7.106 1.675-30.527 3.164-32.846 2.089zm-8.408-19.899c0-1.1-.6-2-1.333-2-.734 0-1.334.9-1.334 2s.6 2 1.333 2c.734 0 1.334-.9 1.334-2zm89.255-8.204c1.53-1.945 2.473-3.845 2.097-4.222-.377-.377-.836-.435-1.02-.13-.182.306-1.787 2.206-3.565 4.223-1.778 2.016-2.571 3.666-1.763 3.666s2.72-1.591 4.25-3.536zm-77.644-1.745c-.82-2.172-1.74-3.7-2.045-3.396-.951.952 1.088 7.345 2.343 7.345.656 0 .522-1.777-.298-3.95zm82.303-27.915c-.837-.837-3.217 2.55-3.184 4.53.012.734.896.178 1.965-1.235 1.07-1.413 1.618-2.896 1.219-3.295zm-66.238-36.904c-1.312-1.312-3.676.702-3.676 3.133 0 2.035.175 2.031 2.254-.047 1.24-1.24 1.88-2.628 1.422-3.086zm39.657.768c4.403-2.196 6.8-3.986 5.333-3.982-2.838.01-16.667 6.028-16.667 7.254 0 1.6 3.717.527 11.333-3.272zm16.667-5.333c0-.733-.9-1.333-2-1.333s-2 .6-2 1.333.9 1.333 2 1.333 2-.6 2-1.333zm-3.334-3.923l5.334-1.104-7.334-.133c-4.033-.073-8.233.45-9.333 1.16-2.539 1.64 3.572 1.682 11.333.077zm35.738-63.976c2.788-1.69 4.765-3.376 4.393-3.748-.947-.947-11.942 5.654-14.237 8.548-1.792 2.258-1.714 2.276 1.44.329a1452.76 1452.76 0 0 1 8.403-5.13z" fill="#ffca28" stroke-width="1.333"/></symbol><symbol viewBox="0 0 400 400" fill-opacity=".05" id="bithound" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.88 0 0 .88 24.121 2.895)" fill="#e53935" fill-opacity="1"><path d="M370.5 207c-1.5-14.8-4.8-29.9-9.5-44-13.5-40.3-38.6-81.6-70.3-110.1-1.4-1.2-6.7-4.4-8.7-3.3-5.2 2.9 4.6 22.8 5.8 26.4 7.4 22 12.1 45.3 6.8 68.3-7.1 30.4-30.4 51.7-61.5 54.3-17.1 1.4-34.3-.5-51.4 1.5-25.6 3-51.7 11.8-68 32.8-1.9 2.4-3.6 5.1-5.2 7.9h-.4c-6.3.7-12.6-2-15.7-3.7-.8-.5-1.6-.9-2.2-1.2-19-10.5-33-34-41.6-53.4-3.9-9-7.2-18.4-9.3-27.9-1-4.3-1.1-8.8-1.3-13.2-.1-2.7.3-6.5-1.2-8.9-3.3-5.2-7.5-.2-8.2 4-1.1 6.9-2.1 13.7-1.8 20.7.5 11.8 3.8 23.5 8 34.5 6.2 16.2 14.9 31.1 26.2 44.4 4.7 5.5 9.7 10.6 15.1 15.3 4.8 4.3 10.9 7.7 14.5 13.2 4.2 6.3 4.9 14.1 4.5 21.4-1 19.3-1.6 37.4 3.9 56.2 4.8 16.7 10.8 33.8 20.8 48.1 5 7.1 11.2 14.6 18 19.9 4.6 3.6 13.3 4 8.3-9.2-11.1-29.3-12.1-59.7 5.2-87.1 14.5-22.8 40.1-43.1 69-39.5 42.5 5.3 72.1 44.3 70 86-.6 11.7-1 21.7-4.7 32.7-1.5 4.4-2.6 10-1.5 14.6 1.8 7.8 10.5 4.9 14.3-.2 10.3-14 21.1-27.6 30.8-42 31.6-47.2 47-101.8 41.3-158.5z"/><path d="M132.4 92.1c.7 2.3 1.4 4.8 1.9 7.5.1 1.1.4 2.3 1 3.4 2.6 6.8 8.9 10.5 14.8 14 3.6 2.2 10.1 4.3 14.1 5.9 5.2 2.1 16.4-.6 21.7-1 12.2-1 23.5-5.3 34.7 1.2-57.4 67.3-3.2 82.3 38.8 49.9 48-37 2.8-124.3 2.8-124.3s-1-6.8-19.2-10.8c-1.7-.9-3.4-1.7-5.1-2.4-18-8.3-34.2 5.3-47.2 16.4-3.8 3.2-7.5 6.4-11.5 9.4-5.4 4-11.2 7.3-17.3 10.2-6.4 3-14 6.4-21.1 6.7-1 0-2.9.2-4.9.6-3.1.3-4.7 1.1-5.4 2.5-1.2 1-2 2.4-1.8 4.2.2 2.5 1.4 4.6 2.7 6.2.4.1.7.3 1 .4z"/></g></symbol><symbol viewBox="0 0 400.00001 399.99999" id="bower" xmlns="http://www.w3.org/2000/svg"><g transform="translate(12.061 33.203) scale(.81733)"><path d="M447.61 200.08c-23.139-22.234-138.85-36.114-175.36-40.154a107.137 107.137 0 0 0 4.517-12.944 146.107 146.107 0 0 1 15.905-5.901c.677 1.997 3.865 9.648 5.682 13.279 73.415 2.025 77.184-54.557 80.17-70.058 2.92-15.157 2.771-29.802 27.953-56.575-37.516-10.933-91.467 16.945-109.54 58.437-6.79-2.545-13.597-4.424-20.328-5.586-4.824-19.46-29.944-73.672-95.863-73.672-83.46 0-174.43 68.853-174.43 185.41 0 97.976 66.891 183.84 104.68 183.84 16.505 0 30.703-12.36 34.036-23.44 2.795 7.597 11.368 31.213 14.184 37.225 4.162 8.89 23.41 16.583 31.833 7.357 10.83 6.017 30.703 9.641 41.534-6.405 20.86 4.412 39.3-8.026 39.702-22.868 10.235-.546 15.256-14.918 13.021-26.363-1.647-8.426-19.248-38.66-26.113-49.098 13.59 11.054 48.013 14.183 52.194.007 21.911 17.198 56.057 8.171 58.765-5.815 26.624 6.917 57.16-8.276 52.146-26.676 42.771-2.958 37.296-48.464 25.296-59.996z" fill="#543729" stroke-width=".973"/><path d="M328.514 103.025c9.212-18.277 20.788-38.234 35.409-50.58-16.093 6.485-31.981 25.873-41.375 46.595a144.914 144.914 0 0 0-14.552-8.132c13.105-27.972 43.555-51.332 77.112-53.157-22.477 20.385-14.498 62.754-32.979 85.183-5.288-5.311-17.43-15.562-23.615-19.909zm-14.53 29.762c.01-.7.272-6.094.763-8.557-1.288-.304-9.3-1.87-13.476-1.772-.304 5.245 2.204 14.17 4.684 19.541 17.075-.358 29.408-5.471 36.667-10.172-6.18-2.88-16.726-5.442-24.745-6.974-.894 1.851-3.097 6.568-3.892 7.934z" fill="#00acee"/><g stroke-width=".973"><path d="M250.54 277.39c.004.024.015.057.018.082-2.165-4.657-4.463-10.314-7.208-17.708 10.688 15.557 44.184 7.533 42.427-6.407 16.395 12.336 50.143-2.055 42.471-19.353 16.423 7.653 35.168-7.745 30.964-14.455 28 5.4 54.832 10.783 63.256 12.938-5.595 9.123-18.339 15.566-37.549 11.089 10.38 14.14-9.773 31.105-37.844 21.76 6.18 13.883-18.814 26.38-47.22 11.91.361 13.889-35.24 15.488-49.315.143zm55.543-70.194c32.497 2.495 86.238 7.34 119.51 11.997-2.102-10.828-7.844-13.921-25.905-18.772-19.425 2.072-68.706 6.913-93.604 6.776z" fill="#2baf2b"/><path d="M285.78 253.36c16.395 12.336 50.143-2.055 42.471-19.353 16.423 7.653 35.168-7.745 30.964-14.455-33.103-6.383-67.84-12.788-75.719-13.908 4.78.254 12.702.797 22.59 1.556 24.899.137 74.18-4.704 93.604-6.775-31.452-7.975-95.666-19.613-140.01-22.48-2.055 3.003-5.833 8.097-12.413 13.51-19.403 41.053-54.557 68.34-93.454 68.34-11.335 0-24.018-1.912-38.233-6.456-8.865 9.497-46.661 16.694-77.329 1.641 24.326 56.961 80.74 94.984 143.19 94.984 52.591 0 75.912-53.704 70.808-67.914-1.238-3.45-6.145-14.889-8.891-22.283 10.689 15.556 44.185 7.532 42.429-6.408z" fill="#ffcc2f"/><path d="M253.91 145.27c4.644-2.526 20.69-12.253 35.981-15.908a67.843 67.843 0 0 1-.536-5.12c-10.032 2.403-28.945 10.51-39.784-.661 22.866 6.9 34.283-6.149 51.09-6.149 10.014 0 24.305 2.798 35.57 7.22-9.061-8.37-38.772-33.63-75.558-33.717-8.213 9.957-17.09 31.526-6.764 54.334z" fill="#cecece"/><path d="M115.58 253.33c14.215 4.544 26.898 6.457 38.233 6.457 38.896 0 74.05-27.29 93.454-68.341-14.351 11.978-39.291 22.228-78.241 22.228 34.694-7.866 64.56-25.156 79.753-50.427-10.68-16.998-22.263-54.603 7.07-84.33-4.512-14.497-26.475-52.766-75.095-52.766-84.85 0-155.17 71.001-155.17 166.15 0 22.525 4.547 43.65 12.67 62.664 30.666 15.054 68.462 7.858 77.327-1.64z" fill="#ef5734"/><path d="M141.03 108.45c0 21.644 17.546 39.191 39.19 39.191s39.192-17.548 39.192-39.191c0-21.644-17.548-39.191-39.192-39.191-21.644 0-39.19 17.547-39.19 39.191z" fill="#ffcc2f"/><path d="M156.76 108.45c0 12.958 10.507 23.463 23.463 23.463 12.96 0 23.464-10.506 23.464-23.463 0-12.959-10.504-23.464-23.464-23.464-12.957 0-23.463 10.506-23.463 23.464z" fill="#543729"/><ellipse cx="180.22" cy="98.044" rx="13.673" ry="8.501" fill="#fff"/></g></g></symbol><symbol viewBox="0 0 140 140" id="browserlist" xmlns="http://www.w3.org/2000/svg"><title>Browserslist logo</title><path d="M70.314 10.066a59.828 59.828 0 0 0-59.828 59.828 59.828 59.828 0 0 0 59.828 59.828 59.828 59.828 0 0 0 59.828-59.828 59.828 59.828 0 0 0-59.828-59.828zm-4.836 8.785c.496 4.043 1.352 7.322 2.572 10.223 4.779-4.287 10.265-7.546 16.041-9.02-.981 3.938-1.357 7.295-1.261 10.43 6.026-2.314 12.349-3.404 18.3-2.706-3.182 2.413-5.482 4.717-7.128 7.015-2.201 12.074 6.858 20.43 14.779 24.551a5.128 5.128 0 0 1 5.183-3.888 5.128 5.128 0 0 1 3.7 8.435v.002c-.487 1.055-2.002 2.343-3.497 3.219-4.075 2.39-11.172 5.736-20.914 7.39.045 1.214.077 2.453.077 3.747 0 4.817-.485 8.291-1.385 10.699-3.3 13.313-12.648 26.76-24.695 31.95.357-4.083.197-7.485-.402-10.591-5.582 3.218-11.646 5.278-17.623 5.52h-.002c1.785-3.662 2.855-6.878 3.412-9.976-6.347.996-12.727.742-18.377-1.17 2.93-2.732 5.054-5.314 6.673-7.96-6.292-1.344-12.169-3.87-16.766-7.686 3.822-1.544 6.795-3.239 9.3-5.197-5.426-3.517-10.034-7.998-12.972-13.23 4.012-.07 7.321-.568 10.3-1.453-3.786-5.215-6.468-11.032-7.333-16.951 3.861 1.405 7.196 2.133 10.36 2.355-1.662-6.22-2.081-12.605-.768-18.436 3.03 2.634 5.824 4.48 8.63 5.815.678-6.406 2.576-12.52 5.893-17.496 1.926 3.622 3.914 6.391 6.111 8.672 2.93-5.754 6.9-10.798 11.791-14.262zm26.465 19.557c-2.395 5.514-1.665 11.297-.555 18.732a2.138 2.138 0 0 0 .28-4.178 3.419 3.419 0 1 1 .092 6.704c.574 3.882 1.157 8.18 1.421 13.125a67.143 67.143 0 0 0 3.25-.649c6.616-1.487 12.258-3.801 16.871-6.506.45-.264.884-.563 1.276-.867.366-.557.333-.957.035-1.285-4.831-1.245-10.891-4.53-15.258-8.795-4.764-4.653-7.428-10.164-7.412-16.281z" fill="#ffca28" stroke-width=".855"/></symbol><symbol viewBox="0 0 140 140" id="browserlist_light" xmlns="http://www.w3.org/2000/svg"><title>Browserslist logo</title><g transform="translate(10.823 10.1)" stroke-width=".855"><circle cx="59.492" cy="59.795" r="59.828" fill="#ffca28"/><path d="M54.656 8.752c-4.89 3.464-8.862 8.508-11.791 14.262-2.198-2.28-4.185-5.05-6.111-8.672-3.318 4.976-5.216 11.09-5.893 17.496-2.807-1.335-5.6-3.18-8.63-5.814-1.314 5.83-.895 12.216.767 18.436-3.164-.223-6.498-.95-10.36-2.356.865 5.92 3.548 11.737 7.333 16.951-2.978.885-6.287 1.383-10.3 1.453 2.939 5.233 7.547 9.714 12.972 13.23-2.505 1.959-5.478 3.654-9.299 5.198 4.596 3.815 10.474 6.341 16.766 7.685-1.62 2.647-3.743 5.228-6.674 7.96 5.65 1.912 12.03 2.166 18.377 1.17-.556 3.098-1.626 6.314-3.412 9.975h.002c5.977-.24 12.042-2.3 17.623-5.52.6 3.108.76 6.51.402 10.593 12.047-5.19 21.395-18.638 24.695-31.951.9-2.408 1.385-5.881 1.385-10.7 0-1.293-.031-2.531-.076-3.745 9.742-1.655 16.839-5.001 20.914-7.39 1.494-.877 3.01-2.165 3.496-3.22v-.002a5.128 5.128 0 0 0-3.7-8.435 5.128 5.128 0 0 0-5.183 3.889c-7.92-4.122-16.98-12.477-14.779-24.551 1.646-2.299 3.947-4.603 7.13-7.016-5.952-.698-12.276.392-18.302 2.707-.095-3.135.28-6.492 1.262-10.43-5.776 1.473-11.262 4.733-16.041 9.02-1.22-2.902-2.076-6.18-2.572-10.223zm26.465 19.557c-.015 6.117 2.648 11.628 7.412 16.281 4.366 4.265 10.426 7.55 15.258 8.795.298.328.331.728-.035 1.285-.392.304-.825.603-1.275.867-4.613 2.704-10.256 5.019-16.871 6.506-1.071.24-2.154.458-3.25.649-.265-4.945-.848-9.243-1.422-13.125a3.419 3.419 0 1 0-.092-6.703 2.138 2.138 0 0 1-.28 4.177c-1.11-7.435-1.84-13.218.555-18.732z" fill="#37474f"/></g></symbol><symbol viewBox="0 0 24 24" id="bucklescript" xmlns="http://www.w3.org/2000/svg"><path d="M3 3v18h18V3H3zm14.1 8.858a5.5 5.5 0 0 1 1.26.145c.417.093.778.213 1.082.357v1.723h-.18a3.281 3.281 0 0 0-.959-.603 2.867 2.867 0 0 0-1.155-.247c-.14 0-.277.011-.416.035a1.4 1.4 0 0 0-.395.12.756.756 0 0 0-.291.231.54.54 0 0 0-.123.348c0 .198.065.35.196.456.13.104.376.2.738.288.237.057.466.11.683.164.22.054.455.128.706.222.496.188.86.444 1.095.77.238.32.357.738.357 1.253 0 .737-.271 1.336-.813 1.798-.538.46-1.27.689-2.197.689a5.447 5.447 0 0 1-1.402-.161 6.725 6.725 0 0 1-1.117-.416v-1.794h.183c.344.318.73.563 1.155.734.429.17.839.256 1.233.256.1 0 .235-.01.4-.03.166-.02.3-.055.403-.102a.97.97 0 0 0 .313-.225c.084-.09.127-.223.127-.4a.568.568 0 0 0-.183-.424c-.119-.12-.294-.213-.526-.276-.243-.067-.5-.128-.773-.185a5.523 5.523 0 0 1-.76-.227c-.544-.204-.936-.48-1.177-.828-.237-.351-.357-.786-.357-1.305 0-.697.27-1.265.81-1.703.54-.442 1.235-.663 2.083-.663zm-8.981.135h2.51c.521 0 .903.02 1.143.06.243.041.484.13.721.266.246.144.43.338.548.583.121.24.181.518.181.83 0 .36-.082.68-.247.959a1.697 1.697 0 0 1-.7.642v.04c.423.098.758.298 1.004.603.249.305.373.706.373 1.205 0 .361-.063.686-.19.97-.125.285-.296.52-.516.707a2.31 2.31 0 0 1-.845.472c-.304.094-.69.141-1.159.141H8.12v-7.478zm1.659 1.372v1.582h.262c.263 0 .486-.007.672-.017.185-.01.332-.043.44-.1.15-.077.248-.175.294-.295.046-.124.07-.266.07-.427a.91.91 0 0 0-.083-.371.518.518 0 0 0-.282-.277 1.187 1.187 0 0 0-.456-.086c-.18-.007-.433-.01-.76-.01h-.157zm0 2.873V18.1H9.9c.469 0 .804-.002 1.007-.006.202-.003.39-.046.56-.13a.712.712 0 0 0 .357-.33c.067-.142.099-.302.099-.483 0-.237-.04-.42-.121-.547-.078-.13-.214-.228-.405-.291a1.842 1.842 0 0 0-.538-.072 49.47 49.47 0 0 0-.716-.003h-.366z" fill="#26a69a" stroke-width="1.067"/></symbol><symbol viewBox="0 0 24 24" id="c" xmlns="http://www.w3.org/2000/svg"><path d="M15.45 15.97l.42 2.44c-.26.14-.68.27-1.24.39-.57.13-1.24.2-2.01.2-2.21-.04-3.87-.7-4.98-1.96-1.14-1.27-1.68-2.88-1.68-4.83C6 9.9 6.68 8.13 8 6.89 9.28 5.64 10.92 5 12.9 5c.75 0 1.4.07 1.94.19s.94.25 1.2.4l-.6 2.49-1.04-.34c-.4-.1-.87-.15-1.4-.15-1.15-.01-2.11.36-2.86 1.1-.76.73-1.14 1.85-1.18 3.34.01 1.36.37 2.42 1.08 3.2.71.77 1.7 1.17 2.99 1.18l1.33-.12c.43-.08.79-.19 1.09-.32z" fill="#0277bd"/></symbol><symbol viewBox="0 0 300 300" id="cabal" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -822.52)" fill-rule="evenodd" color="#000"><rect transform="matrix(-.98339 .18149 .60192 .79856 0 0)" x="405.55" y="967.22" width="107.25" height="156.59" rx="12.306" ry="12.31" fill="#2d9bbd"/><rect transform="matrix(-.98528 .17093 -.59175 .80612 0 0)" x="-1156.5" y="1461.9" width="108.34" height="123.15" rx="10.69" ry="12.31" fill="#4a4bcd"/><path d="M52.112 965.158c-1.343 3.515-26.292 23.248-25.744 27.277.548 4.03 29.812 16.023 32.04 19.027s10.545 41.668 13.603 42.5 18.828-31.274 21.548-32.932c2.72-1.658 32.808 2.503 34.15-1.01 1.343-3.515-18.174-35.352-18.721-39.381-.548-4.03 9.732-40.12 7.502-43.125-2.229-3.005-30.06 9.427-33.118 8.594-3.059-.833-26.793-27.3-29.514-25.643-2.72 1.657-.405 41.177-1.747 44.693z" fill="#2e5bc1"/></g></symbol><symbol viewBox="0 0 24 24" id="cake" xmlns="http://www.w3.org/2000/svg"><path d="M12.254 6.621a1.807 1.807 0 0 0 1.808-1.807c0-.344-.09-.66-.262-.932l-1.546-2.684-1.546 2.684a1.72 1.72 0 0 0-.262.932 1.808 1.808 0 0 0 1.808 1.807m4.158 9.04l-.967-.976-.976.976c-1.175 1.166-3.236 1.175-4.42 0l-.959-.976-.994.976a3.134 3.134 0 0 1-3.977.353v4.167a.904.904 0 0 0 .904.904h14.463a.904.904 0 0 0 .904-.904v-4.167a3.134 3.134 0 0 1-3.977-.353m1.265-6.328h-4.52V7.525H11.35v1.808H6.83a2.712 2.712 0 0 0-2.711 2.712v1.392c0 .977.795 1.772 1.771 1.772.489 0 .94-.18 1.248-.515l1.952-1.926 1.908 1.926c.669.669 1.835.669 2.504 0l1.916-1.926 1.944 1.926c.316.334.768.515 1.247.515.976 0 1.78-.795 1.78-1.772v-1.392a2.712 2.712 0 0 0-2.711-2.712z" fill="#ff7043" stroke-width=".904"/></symbol><symbol viewBox="0 0 24 24" id="certificate" xmlns="http://www.w3.org/2000/svg"><path d="M4 3c-1.11 0-2 .89-2 2v10a2 2 0 0 0 2 2h8v5l3-3 3 3v-5h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H4m8 2l3 2 3-2v3.5l3 1.5-3 1.5V15l-3-2-3 2v-3.5L9 10l3-1.5V5M4 5h5v2H4V5m0 4h3v2H4V9m0 4h5v2H4v-2z" fill="#ff5722"/></symbol><symbol viewBox="0 0 24 24" id="changelog" xmlns="http://www.w3.org/2000/svg"><path d="M11 7v5.11l4.71 2.79.79-1.28-4-2.37V7m0-5C8.97 2 5.91 3.92 4.27 6.77L2 4.5V11h6.5L5.75 8.25C6.96 5.73 9.5 4 12.5 4a7.5 7.5 0 0 1 7.5 7.5 7.5 7.5 0 0 1-7.5 7.5c-3.27 0-6.03-2.09-7.06-5h-2.1c1.1 4.03 4.77 7 9.16 7 5.24 0 9.5-4.25 9.5-9.5A9.5 9.5 0 0 0 12.5 2z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 24 24" id="clojure" xmlns="http://www.w3.org/2000/svg"><path d="M3.355 1.78c-.845 0-1.525.68-1.525 1.525v17.441c0 .845.68 1.525 1.525 1.525h17.442c.845 0 1.525-.68 1.525-1.525V3.305c0-.845-.68-1.526-1.525-1.526H3.355zm6.168 2.572h1.963l6.368 14.931H15.93l-3.38-8.086-3.349 8.086H7.21l4.346-10.38-2.032-4.551z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="cmake" xmlns="http://www.w3.org/2000/svg"><path d="M11.99 2.965L2.977 20.999l9.874-8.47-.863-9.564z" fill="#1e88e5"/><path d="M12.007 2.963l.002.29 1.312 14.498-.001.006.023.26 7.362 2.979h.416l-.158-.311-.114-.228h-.002l-8.84-17.494z" fill="#e53935"/><path d="M8.607 16.11L2.98 20.995h17.743v-.016L8.607 16.11z" fill="#7cb342"/></symbol><symbol class="bfmain_logo__svg" viewBox="0 0 300 300.00001" id="code-climate" xmlns="http://www.w3.org/2000/svg"><path class="bfsymbol" d="M196.19 75.562l-51.846 51.561 30.766 30.766 21.08-21.08 59.252 59.537 30.481-30.766zm-61.246 60.961l-30.481-30.481-78.053 78.053-11.964 11.964 30.766 30.766 11.964-12.249 39.596-39.312 7.691-7.691 30.481 30.48 28.772 28.773 30.766-30.766-28.772-28.772z" fill="#eee" stroke-width="2.849"/></symbol><symbol class="bgmain_logo__svg" viewBox="0 0 300 300.00001" id="code-climate_light" xmlns="http://www.w3.org/2000/svg"><path class="bgsymbol" d="M196.19 75.562l-51.846 51.561 30.766 30.766 21.08-21.08 59.252 59.537 30.481-30.766zm-61.246 60.961l-30.481-30.481-78.053 78.053-11.964 11.964 30.766 30.766 11.964-12.249 39.596-39.312 7.691-7.691 30.481 30.48 28.772 28.773 30.766-30.766-28.772-28.772z" fill="#455a64" stroke-width="2.849"/></symbol><symbol viewBox="0 0 24 24" id="coffee" xmlns="http://www.w3.org/2000/svg"><path d="M2 21h18v-2H2M20 8h-2V5h2m0-2H4v10a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4v-3h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="coldfusion" xmlns="http://www.w3.org/2000/svg"><rect transform="rotate(90)" x="2.283" y="-21.86" width="19.487" height="19.487" ry="0" fill="#0d3858" stroke="#4dd0e1" stroke-width=".7"/><text x="6.653" y="16.426" fill="#4dd0e1" font-family="Calibri" font-size="29.001" font-weight="bold" letter-spacing="0" stroke-width=".725" word-spacing="0" style="line-height:1.25"><tspan x="6.653" y="16.426" font-family="'Segoe UI'" font-size="10.634" font-weight="normal">C<tspan font-size="11.844">f</tspan></tspan></text></symbol><symbol viewBox="0 0 24 24" id="conduct" xmlns="http://www.w3.org/2000/svg"><path d="M10 17l-4-4 1.41-1.41L10 14.17l6.59-6.59L18 9m-6-6a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#cddc39"/></symbol><symbol viewBox="0 0 24 24" id="console" xmlns="http://www.w3.org/2000/svg"><path d="M20 19V7H4v12h16m0-16a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16m-7 14v-2h5v2h-5m-3.42-4L5.57 9H8.4l3.3 3.3c.39.39.39 1.03 0 1.42L8.42 17H5.59z" fill="#ff7043"/></symbol><symbol viewBox="0 0 24 24" id="contributing" xmlns="http://www.w3.org/2000/svg"><path d="M17 9H7V7h10m0 6H7v-2h10m-3 6H7v-2h7M12 3a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m7 0h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="cpp" xmlns="http://www.w3.org/2000/svg"><path d="M10.5 15.97l.41 2.44c-.26.14-.68.27-1.24.39-.57.13-1.24.2-2.01.2-2.21-.04-3.87-.7-4.98-1.96C1.56 15.77 1 14.16 1 12.21c.05-2.31.72-4.08 2-5.32C4.32 5.64 5.96 5 7.94 5c.75 0 1.4.07 1.94.19s.94.25 1.2.4l-.58 2.49-1.06-.34c-.4-.1-.86-.15-1.39-.15-1.16-.01-2.12.36-2.87 1.1-.76.73-1.15 1.85-1.18 3.34 0 1.36.37 2.42 1.08 3.2.71.77 1.71 1.17 2.99 1.18l1.33-.12c.43-.08.79-.19 1.1-.32M11 11h2V9h2v2h2v2h-2v2h-2v-2h-2v-2m7 0h2V9h2v2h2v2h-2v2h-2v-2h-2v-2z" fill="#0277bd"/></symbol><symbol viewBox="0 0 24 24" id="credits" xmlns="http://www.w3.org/2000/svg"><path d="M3 3h18v2H3V3m4 4h10v2H7V7m-4 4h18v2H3v-2m4 4h10v2H7v-2m-4 4h18v2H3v-2z" fill="#9ccc65"/></symbol><symbol viewBox="0 0 200 200" id="crystal" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:none}</style><path d="M179.363 121.67l-57.623 57.507c-.23.23-.576.346-.806.23l-78.713-21.09c-.346-.115-.577-.345-.577-.576L20.44 79.144c-.115-.345 0-.576.23-.806L78.294 20.83c.23-.23.576-.346.807-.23l78.713 21.09c.345.114.576.345.576.575l21.09 78.597c.23.346.115.577-.115.807zM102.148 59.09l-77.33 20.63c-.115 0-.23.23-.115.345l56.586 56.47c.115.115.346.115.346-.115l20.744-77.215c.115 0-.115-.23-.23-.116z" stroke-width="1.153" fill="#cfd8dc"/></symbol><symbol viewBox="0 0 200 200" id="crystal_light" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:none}</style><path d="M179.363 121.67l-57.623 57.507c-.23.23-.576.346-.806.23l-78.713-21.09c-.346-.115-.577-.345-.577-.576L20.44 79.144c-.115-.345 0-.576.23-.806L78.294 20.83c.23-.23.576-.346.807-.23l78.713 21.09c.345.114.576.345.576.575l21.09 78.597c.23.346.115.577-.115.807zM102.148 59.09l-77.33 20.63c-.115 0-.23.23-.115.345l56.586 56.47c.115.115.346.115.346-.115l20.744-77.215c.115 0-.115-.23-.23-.116z" fill="#37474f" stroke-width="1.153"/></symbol><symbol viewBox="0 0 24 24" id="csharp" xmlns="http://www.w3.org/2000/svg"><path d="M11.5 15.97l.41 2.44c-.26.14-.68.27-1.24.39-.57.13-1.24.2-2.01.2-2.21-.04-3.87-.7-4.98-1.96C2.56 15.77 2 14.16 2 12.21c.05-2.31.72-4.08 2-5.32C5.32 5.64 6.96 5 8.94 5c.75 0 1.4.07 1.94.19s.94.25 1.2.4l-.58 2.49-1.06-.34c-.4-.1-.86-.15-1.39-.15-1.16-.01-2.12.36-2.87 1.1-.76.73-1.15 1.85-1.18 3.34 0 1.36.37 2.42 1.08 3.2.71.77 1.71 1.17 2.99 1.18l1.33-.12c.43-.08.79-.19 1.1-.32M13.89 19l.61-4H13l.34-2h1.5l.32-2h-1.5L14 9h1.5l.61-4h2l-.61 4h1l.61-4h2l-.61 4H22l-.34 2h-1.5l-.32 2h1.5L21 15h-1.5l-.61 4h-2l.61-4h-1l-.61 4h-2m2.95-6h1l.32-2h-1l-.32 2z" fill="#0277bd"/></symbol><symbol viewBox="0 0 24 24" id="css" xmlns="http://www.w3.org/2000/svg"><path d="M5 3l-.65 3.34h13.59L17.5 8.5H3.92l-.66 3.33h13.59l-.76 3.81-5.48 1.81-4.75-1.81.33-1.64H2.85l-.79 4 7.85 3 9.05-3 1.2-6.03.24-1.21L21.94 3H5z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="css-map" xmlns="http://www.w3.org/2000/svg"><path d="M18 8v2h2v10H10v-2H8v4h14V8h-4z" fill="#42a5f5"/><path d="M4.676 3l-.488 2.51h10.211l-.33 1.623H3.864l-.496 2.502H13.58l-.57 2.863-4.119 1.36-3.569-1.36.248-1.232H3.06l-.593 3.005 5.898 2.254 6.8-2.254.902-4.53.18-.91L17.406 3H4.675z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 33 33" id="cucumber" xmlns="http://www.w3.org/2000/svg"><title>cucumber-mark-transparent-pips</title><g transform="translate(0 -5)" fill="none" fill-rule="evenodd"><path d="M-4-1h40v40H-4z"/><path d="M16.641 7.092c-7.028 0-12.714 5.686-12.714 12.714 0 6.187 4.435 11.327 10.288 12.471v3.64C21.824 34.77 28.561 28.73 29.063 20.8c.303-4.772-2.076-9.644-6.09-12.01a10.575 10.575 0 0 0-1.455-.728l-.243-.097c-.223-.082-.448-.175-.68-.242a12.614 12.614 0 0 0-3.954-.632zm2.62 4.707a1.387 1.387 0 0 0-1.213.485c-.233.31-.379.611-.534.923-.466 1.087-.31 2.251.388 3.105 1.087-.233 2.01-.927 2.475-2.014a2.45 2.45 0 0 0 .243-1.02c.048-.824-.634-1.404-1.359-1.479zm-5.654.073c-.708.068-1.382.63-1.382 1.407 0 .31.087.709.243 1.02.466 1.086 1.46 1.78 2.546 2.013.621-.854.782-2.018.316-3.105-.155-.311-.3-.617-.534-.85a1.364 1.364 0 0 0-1.188-.485zm-3.809 3.735c-1.224.063-1.77 1.602-.752 2.402.31.233.612.403.922.559 1.087.466 2.344.306 3.275-.316-.233-1.009-1.023-1.936-2.11-2.402-.388-.155-.703-.243-1.092-.243-.087-.009-.161-.004-.243 0zm11.961 4.708a3.551 3.551 0 0 0-2.013.582c.233 1.01 1.023 1.936 2.11 2.401.389.156.705.244 1.093.244 1.397.077 2.08-1.65.994-2.427-.31-.233-.611-.379-.922-.534a3.354 3.354 0 0 0-1.262-.266zm-10.603.072a3.376 3.376 0 0 0-1.261.267c-.389.155-.69.325-.923.558-1.009.854-.33 2.48 1.068 2.402.388 0 .782-.087 1.092-.243 1.087-.465 1.859-1.392 2.014-2.401a3.474 3.474 0 0 0-1.99-.582zm3.931 2.378c-1.087.233-2.009.927-2.475 2.014-.155.31-.243.684-.243.995-.077 1.32 1.724 2.028 2.5 1.02.233-.312.378-.613.534-.923.466-1.01.306-2.174-.316-3.106zm2.887.073c-.621.854-.781 2.019-.315 3.106.155.31.3.615.534.848.854.932 2.65.243 2.572-.921 0-.31-.088-.71-.243-1.02-.466-1.087-1.46-1.78-2.547-2.013z" fill="#4caf50" stroke-width=".776"/></g></symbol><symbol id="cuda" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><style>.bust0{fill:#76b900}</style><title>NVIDIA-Logo</title><path id="buEye_Mark" class="bust0" d="M76.362 75.199V64.116c1.095-.068 2.19-.137 3.284-.137 30.377-.958 50.286 26.135 50.286 26.135s-21.483 29.83-44.539 29.83c-3.079 0-6.089-.48-8.962-1.438v-33.66c11.836 1.436 14.23 6.636 21.277 18.471l15.804-13.273s-11.562-15.12-30.992-15.12c-2.053-.068-4.105.069-6.158.274m0-36.67v16.556l3.284-.205c42.213-1.437 69.784 34.618 69.784 34.618s-31.608 38.45-64.516 38.45c-2.873 0-5.678-.274-8.483-.753v10.262c2.326.274 4.72.48 7.046.48 30.65 0 52.817-15.668 74.3-34.14 3.558 2.874 18.13 9.784 21.14 12.794-20.388 17.104-67.937 30.856-94.893 30.856-2.6 0-5.062-.137-7.525-.41v14.436h116.44V38.532zm0 79.977v8.757C48.038 122.2 40.17 92.712 40.17 92.712s13.615-15.05 36.192-17.514v9.579h-.068c-11.836-1.437-21.14 9.646-21.14 9.646s5.268 18.678 21.209 24.082M26.077 91.481S42.839 66.714 76.43 64.115v-9.03C39.213 58.094 7.057 89.565 7.057 89.565s18.199 52.68 69.305 57.47v-9.579c-37.492-4.652-50.286-45.975-50.286-45.975z" fill="#8bc34a" stroke-width=".684"/></symbol><symbol viewBox="0 0 24 24" id="dart" xmlns="http://www.w3.org/2000/svg"><title>Dart</title><path d="M12.486 1.385a.978.978 0 0 0-.682.281l-.01.007-6.387 3.692 6.371 6.372v.004l7.659 7.659 1.46-2.63-5.265-12.64-2.456-2.457a.972.972 0 0 0-.69-.288z" fill="#00ca94"/><path d="M5.422 5.35L1.73 11.733l-.007.01a.967.967 0 0 0 .006 1.371l3.059 3.061 11.963 4.706 2.704-1.502-.073-.073-.018.002-7.5-7.512h-.01L5.423 5.35z" fill="#1565c0"/><path d="M5.405 5.353l6.518 6.525h.01l7.502 7.51 2.855-.544.005-8.449-3.016-2.955c-.66-.647-1.675-1.064-2.695-1.202l.002-.032-11.181-.853z" fill="#1565c0"/><path d="M5.414 5.361l6.521 6.522v.009l7.506 7.506-.546 2.855h-8.448l-2.954-3.017c-.647-.66-1.064-1.676-1.2-2.696l-.033.003L5.414 5.36z" fill="#00ee94"/></symbol><symbol viewBox="0 0 24 24" id="database" xmlns="http://www.w3.org/2000/svg"><path d="M12 3C7.58 3 4 4.79 4 7s3.58 4 8 4 8-1.79 8-4-3.58-4-8-4M4 9v3c0 2.21 3.58 4 8 4s8-1.79 8-4V9c0 2.21-3.58 4-8 4s-8-1.79-8-4m0 5v3c0 2.21 3.58 4 8 4s8-1.79 8-4v-3c0 2.21-3.58 4-8 4s-8-1.79-8-4z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="diff" xmlns="http://www.w3.org/2000/svg"><path d="M3 1c-1.11 0-2 .89-2 2v11c0 1.11.89 2 2 2h2v-2H3V3h11v2h2V3c0-1.11-.89-2-2-2H3m6 6c-1.11 0-2 .89-2 2v2h2V9h2V7H9m4 0v2h1v1h2V7h-3m5 0v2h2v11H9v-2H7v2c0 1.11.89 2 2 2h11c1.11 0 2-.89 2-2V9c0-1.11-.89-2-2-2h-2m-4 5v2h-2v2h2c1.11 0 2-.89 2-2v-2h-2m-7 1v3h3v-2H9v-1H7z" fill="#42a5f5"/></symbol><symbol id="docker" viewBox="0 0 41 34.5" xmlns="http://www.w3.org/2000/svg"><style id="bystyle2">.byst0{fill:#fff}.byst1{clip-path:url(#bySVGID_4_)}</style><g id="byg34" transform="translate(.292 1.9)" fill="#0087c9"><g id="byg32"><g id="byg30"><g id="byg28"><g id="byg26"><g id="byg9"><path id="bySVGID_1_" class="byst0" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2c1.2 0 2.1.9 2.1 2s-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></g></g></g></g></g></g></symbol><symbol viewBox="0 0 24 24" id="document" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m9 16v-2H6v2h9m3-4v-2H6v2h12z" fill="#42a5f5"/></symbol><symbol preserveAspectRatio="xMidYMid" viewBox="0 0 200 200" id="drone" xmlns="http://www.w3.org/2000/svg"><g fill="#e0e0e0" transform="translate(9.063 22.346) scale(.71044)"><path d="M128.22.723C32.095.723.39 84.566.39 115.222h77.928S89.36 75.275 128.22 75.275s49.906 39.947 49.906 39.947h77.476c0-30.66-31.257-114.5-127.38-114.5m98.82 134.45h-48.914s-8.55 39.946-49.906 39.946c-41.355 0-49.902-39.948-49.902-39.948H30.255c0 10.25 37.727 82.708 98.443 82.708 60.714 0 98.344-59.604 98.344-82.708"/><circle cx="128" cy="126.08" r="32.768"/></g></symbol><symbol preserveAspectRatio="xMidYMid" viewBox="0 0 200 200" id="drone_light" xmlns="http://www.w3.org/2000/svg"><g fill="#424242" transform="translate(9.063 22.346) scale(.71044)"><path d="M128.22.723C32.095.723.39 84.566.39 115.222h77.928S89.36 75.275 128.22 75.275s49.906 39.947 49.906 39.947h77.476c0-30.66-31.257-114.5-127.38-114.5m98.82 134.45h-48.914s-8.55 39.946-49.906 39.946c-41.355 0-49.902-39.948-49.902-39.948H30.255c0 10.25 37.727 82.708 98.443 82.708 60.714 0 98.344-59.604 98.344-82.708"/><circle cx="128" cy="126.08" r="32.768"/></g></symbol><symbol viewBox="0 0 3473 3473" id="editorconfig" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" xmlns="http://www.w3.org/2000/svg"><defs id="ccdefs4"><style id="ccstyle2">.ccfil2{fill:#020202}.ccfil0{fill:#e3e3f8}.ccfil5{fill:#efefef}.ccfil6{fill:#faf1f1}.ccfil3{fill:#fdf2f2}.ccfil1{fill:#fdfdfd}.ccfil4{fill:#fef3f3}</style></defs><g id="ccLayer_x0020_1" transform="matrix(.8945 0 0 .8945 138.649 275.985)"><g id="cc_631799120"><g id="ccg11"><path class="ccfil0" d="M967 1895c46-30 84-105 61-158-63 27-60 89-61 158z" id="ccpath7" fill="#e3e3f8"/><path class="ccfil0" d="M1679 2067c50-16 98-72 71-130-39 27-64 64-71 130z" id="ccpath9" fill="#e3e3f8"/></g><g id="ccg21"><path class="ccfil1" d="M280 2895c0 63 16 131 60 155 162 91 730 20 923-23 101-23 183-98 278-139 214-93 369-168 540-293 124-91 321-347 342-500l-169-38c-4 172-43 211-196 251-103 28-304 34-409 16-139-23-202-96-265-179-122-162 27-275-166-286-203 249-561 70-718 45-67 97-224 727-222 871 97-33 158 3 245 37 308 119 39 224-84 193-84-20-110-75-159-110z" id="ccpath13" fill="#fdfdfd"/><path class="ccfil1" d="M683 1458c125 24 236 76 342 129 173 86 204 74 220 194 2 22-2 34 61 54 106 33-61-26 223-25 169 1 556 69 681 148 52 33 42 75 218 70-2-207-57-516-138-706-99-230-230-265-497-351-156-50-614-105-756-17-133 83-158 182-282 356-36 51-49 90-72 148z" id="ccpath15" fill="#fdfdfd"/><path class="ccfil1" d="M1784 1883c100 41-5 306-144 242-45-127 62-199 91-256-60-9-231-36-282-17-66 25-81 166-47 232 160 314 867 247 792 3-30-99-58-115-159-149-81-27-162-55-251-55z" id="ccpath17" fill="#fdfdfd"/><path class="ccfil1" d="M527 1848c80 77 261 89 378 95 15-155 28-271 152-262 61 83 29 181-35 244 109-1 172-83 156-202-92-66-371-198-511-217-39 42-135 272-140 342z" id="ccpath19" fill="#fdfdfd"/></g><path class="ccfil2" d="M339 2838c66-6 238 44 252 100-107 13-243 3-252-100zm-59 57c49 35 75 90 159 110 123 31 392-74 84-193-87-34-148-70-245-37-2-144 155-774 222-871 157 25 515 204 718-45 193 11 44 124 166 286 63 83 126 156 265 179 105 18 306 12 409-16 153-40 192-79 196-251l169 38c-21 153-218 409-342 500-171 125-326 200-540 293-95 41-177 116-278 139-193 43-761 114-923 23-44-24-60-92-60-155zm1399-828c7-66 32-103 71-130 27 58-21 114-71 130zm105-184c89 0 170 28 251 55 101 34 129 50 159 149 75 244-632 311-792-3-34-66-19-207 47-232 51-19 222 8 282 17-29 57-136 129-91 256 139 64 244-201 144-242zm-817 12c1-69-2-131 61-158 23 53-15 128-61 158zm-440-47c5-70 101-300 140-342 140 19 419 151 511 217 16 119-47 201-156 202 64-63 96-161 35-244-124-9-137 107-152 262-117-6-298-18-378-95zm-100-80c-37-102-37-261 120-274l-80 223c-21 48-21 37-40 51zm256-310c23-58 36-97 72-148 124-174 149-273 282-356 142-88 600-33 756 17 267 86 398 121 497 351 81 190 136 499 138 706-176 5-166-37-218-70-125-79-512-147-681-148-284-1-117 58-223 25-63-20-59-32-61-54-16-120-47-108-220-194-106-53-217-105-342-129zm1770-49c-19-63 16-59 77-102 35-25 63-51 106-75 161-90 461-105 589 2 52 43 137 127 124 237-27 219-177 339-300 439-125 102-333 207-548 137-18-44-4-323-25-426-19-92-9-102 44-157 156-162 494-280 686-141 81 60 58 83 100 129 52-56-45-244-403-232-243 8-348 198-450 189zM997 840c5-139 133-427 261-527 155-120 317-233 555-98 59 33 56 50 62 132 5 79-2 108-22 172-158 510-290 217-796 338 19-166 163-314 243-391 137-133 236-219 442-191 57 95 63 155-6 266-92 148-115 139-101 240 72-18 94-88 127-158 201-420-91-471-270-394-120 51-334 287-404 429-14 28-29 64-42 95zm792 21c21-125 145-156 145-541 0-166-204-315-471-204-229 94-264 166-386 350-115 174-111 365-210 526-29 46-55 62-87 108-23 34-40 77-63 117-47 77-95 133-133 225-120 3-221 5-233 129-16 170 64 212 64 276-1 69-281 765-203 1180 22 114 97 115 217 129 289 35 664 23 923-81l470-225c119-67 319-194 408-287 63-65 96-120 150-197 74-108 76-106 92-253 98 18 281 61 342 114-7 69-41 36-41 98 39 1 104-48 120-102-41-60-84-50-143-98 47-37 132-54 197-81 140-58 379-234 438-394 47-129 12-344-64-428-80-88-266-133-418-133-181 0-368 130-514 186-56-49-60-105-101-159-47-64-353-224-499-255z" id="ccpath23" fill="#020202"/><path class="ccfil3" d="M2453 1409c102 9 207-181 450-189 358-12 455 176 403 232-42-46-19-69-100-129-192-139-530-21-686 141-53 55-63 65-44 157 21 103 7 382 25 426 215 70 423-35 548-137 123-100 273-220 300-439 13-110-72-194-124-237-128-107-428-92-589-2-43 24-71 50-106 75-61 43-96 39-77 102z" id="ccpath25" fill="#fdf2f2"/><path class="ccfil4" d="M997 840l49-87c13-31 28-67 42-95 70-142 284-378 404-429 179-77 471-26 270 394-33 70-55 140-127 158-14-101 9-92 101-240 69-111 63-171 6-266-206-28-305 58-442 191-80 77-224 225-243 391 506-121 638 172 796-338 20-64 27-93 22-172-6-82-3-99-62-132-238-135-400-22-555 98-128 100-256 388-261 527z" id="ccpath27" fill="#fef3f3"/><path class="ccfil5" d="M427 1768c19-14 19-3 40-51l80-223c-157 13-157 172-120 274z" id="ccpath29" fill="#efefef"/><path class="ccfil6" d="M591 2938c-14-56-186-106-252-100 9 103 145 113 252 100z" id="ccpath31" fill="#faf1f1"/></g></g></symbol><symbol viewBox="0 0 24 24" id="elixir" xmlns="http://www.w3.org/2000/svg"><path d="M12.431 22.383c-3.86 0-6.99-3.64-6.99-8.13 0-3.678 2.774-8.172 4.916-10.91 1.014-1.295 2.931-2.321 2.931-2.321s-.982 5.238 1.683 7.318c2.365 1.847 4.105 4.25 4.105 6.363 0 4.232-2.784 7.68-6.645 7.68z" fill="#9575cd" stroke-width="1.256"/></symbol><symbol viewBox="0 0 323.00001 322.99999" id="elm" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.8053 0 0 .8053 30.106 31.524)"><path fill="#f0ad00" d="M160.8 153.865l68.028-68.03H92.77z"/><path fill="#7fd13b" d="M160.983 5.098H12.033l68.524 68.525H229.51z"/><path fill="#7fd13b" stroke-width=".974" d="M243.906 88.021l74.136 74.137-74.474 74.475-74.137-74.137z"/><path fill="#60b5cc" d="M318.2 145.045V5.098H178.252z"/><path fill="#5a6378" d="M152.164 162.499L3.4 13.733v297.533z"/><path fill="#f0ad00" d="M252.205 245.27l65.995 65.996v-131.99z"/><path fill="#60b5cc" d="M160.8 171.134L12.034 319.899h297.53z"/></g></symbol><symbol viewBox="0 0 24 24" id="email" xmlns="http://www.w3.org/2000/svg"><path d="M20 8l-8 5-8-5V6l8 5 8-5m0-2H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 30 30" id="erlang" xmlns="http://www.w3.org/2000/svg"><path style="line-height:1.25;-inkscape-font-specification:'Wide Latin'" d="M5.217 4.367c-.048.052-.097.1-.144.153C2.697 7.182 1.51 10.798 1.51 15.366c0 4.418 1.156 7.862 3.46 10.34h19.414c2.553-1.152 4.127-3.43 4.127-3.43l-3.147-2.52-1.454 1.381c-.866.773-.845.931-2.314 1.78-1.496.674-3.04.966-4.634.966-2.516 0-4.423-.909-5.723-2.059-1.286-1.15-1.985-4.511-2.097-6.68l17.458.067-.182-1.472s-.847-7.129-2.542-9.372zm8.76.846c1.565 0 3.22.535 3.96 1.471.742.937.932 1.667.974 3.524H9.12c.111-1.955.436-2.81 1.372-3.697.937-.888 2.03-1.298 3.484-1.298z" font-weight="400" font-size="48" font-family="Wide Latin" letter-spacing="0" word-spacing="0" fill="#f44336" stroke-width=".97"/></symbol><symbol viewBox="0 0 299.99999 300.00001" id="eslint" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-2.88 18.438) scale(1.0344)"><path d="M97.021 99.016l48.432-27.962c1.212-.7 2.706-.7 3.918 0l48.433 27.962a3.92 3.92 0 0 1 1.959 3.393v55.924a3.924 3.924 0 0 1-1.959 3.394l-48.433 27.962c-1.212.7-2.706.7-3.918 0l-48.432-27.962a3.92 3.92 0 0 1-1.959-3.394v-55.924a3.922 3.922 0 0 1 1.959-3.393" fill="#7986cb"/><path d="M273.34 124.49L215.473 23.82c-2.102-3.64-5.985-6.325-10.188-6.325H89.545c-4.204 0-8.088 2.685-10.19 6.325L21.488 124.27c-2.102 3.641-2.102 8.236 0 11.877l57.867 99.847c2.102 3.64 5.986 5.501 10.19 5.501h115.74c4.203 0 8.087-1.805 10.188-5.446l57.867-100.01c2.104-3.639 2.104-7.907.001-11.547m-47.917 48.41c0 1.48-.891 2.849-2.174 3.59l-73.71 42.527a4.194 4.194 0 0 1-4.17 0l-73.767-42.527c-1.282-.741-2.179-2.109-2.179-3.59V87.847c0-1.481.884-2.849 2.167-3.59l73.707-42.527a4.185 4.185 0 0 1 4.168 0l73.772 42.527c1.283.741 2.186 2.109 2.186 3.59z" fill="#3f51b5"/></g></symbol><symbol viewBox="0 0 24 24" id="exe" xmlns="http://www.w3.org/2000/svg"><path d="M19 4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h14m0 14V8H5v10h14z" fill="#e64a19"/></symbol><symbol viewBox="0 0 24 24" id="favicon" xmlns="http://www.w3.org/2000/svg"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.62L12 2 9.19 8.62 2 9.24l5.45 4.73L5.82 21 12 17.27z" fill="#ffd54f"/></symbol><symbol viewBox="0 0 24 24" id="file" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m5 2H6v16h12v-9h-7V4z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 400 400" id="firebase" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 103)"><path d="M72.55 208.77l44.456-292.29 56.209 90.445L195.49-37.57 330.6 209.28z" fill="#ffa712"/><path d="M195.7 276.73l134.9-67.45-46.5-224.83L72.55 208.77z" fill="#fcca3f"/><path d="M173.22 6.932L72.56 208.772l136.35-144.58z" fill="#f6820c"/></g></symbol><symbol viewBox="0 0 24 24" id="flash" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="cma"><stop offset="0" stop-color="#d92f3c"/><stop offset="1" stop-color="#791223"/></linearGradient><linearGradient xlink:href="#cma" id="cmb" x1="2.373" y1="12.027" x2="21.86" y2="12.027" gradientUnits="userSpaceOnUse" gradientTransform="translate(-.09 -24.144)"/></defs><rect width="19.487" height="19.487" x="2.283" y="-21.86" transform="rotate(90)" ry="0" fill="url(#cmb)"/><path style="line-height:125%" d="M16.802 5.768l-.013.002a6.43 6.43 0 0 0-1.182.192 5.062 5.062 0 0 0-1.494.718c-.428.323-.817.72-1.17 1.191-.34.48-.682 1.032-1.022 1.66-.12.228-.233.424-.35.636v.002h-.004l-1.34 2.394-.005-.002c-.238.443-.461.847-.665 1.198a4.358 4.358 0 0 1-.716.94 2.79 2.79 0 0 1-.907.594c-.072.027-.161.042-.242.063h-.989v2.414h.989v-.002a6.427 6.427 0 0 0 1.185-.192 5.062 5.062 0 0 0 1.494-.718 5.94 5.94 0 0 0 1.171-1.191c.34-.48.681-1.033 1.021-1.66.12-.228.235-.425.353-.637l.006.002.003-.005.037-.066h2.53v.002h1.124v-2.408h-.33v-.001h-1.98c.22-.407.432-.789.621-1.115.214-.37.452-.682.717-.94a2.79 2.79 0 0 1 .906-.594c.07-.027.16-.041.239-.061h.992V8.18h-.002V5.77h-.977v-.002z" font-weight="400" font-size="40" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#fff"/></symbol><symbol class="cnflow-logo" viewBox="0 0 299.99999 300" id="flow" xmlns="http://www.w3.org/2000/svg"><title>Flow logo</title><path d="M38.75 33.427l77.461 77.47H54.436l61.145 61.16H38.437l93.462 93.478v-77.158l.01-.01v-77.47h-.01V66.982h46.691l20.394 20.393H153.57v76.531h22.05l24.474 24.473h-15.806l-.01-.01v.01h-31.665l-.01-.01v.01h-.313l.313.313v77.148h109.149l-39.2-39.2v-15.806l8.465 8.466v-77.37h-15.682l.017-38.191 30.09 30.086V56.362h-64.874l-22.94-22.934H113.71z" fill="#fbc02d" fill-opacity=".976" stroke-width=".955" class="cnflow-logo-mark"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-aurelia" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="coa" x1="-388.15%" x2="237.68%" y1="-144.18%" y2="430.41%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cob" x1="72.945%" x2="-97.052%" y1="84.424%" y2="-147.7%"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="coc" x1="-283.88%" x2="287.54%" y1="-693.6%" y2="101.71%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cod" x1="-821.19%" x2="101.99%" y1="-469.05%" y2="288.24%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="coe" x1="-140.36%" x2="419.01%" y1="-230.93%" y2="261.98%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cof" x1="191.08%" x2="20.358%" y1="253.95%" y2="20.403%"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="cog" x1="-388.09%" x2="237.67%" y1="-173.85%" y2="518.99%"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="coi" x1="-31.824" x2="19.682" y1="-11.741" y2="35.548" gradientTransform="scale(.95818 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#coa"/><linearGradient id="coj" x1="12.022" x2="-15.716" y1="13.922" y2="-23.952" gradientTransform="scale(.96226 1.0392)" gradientUnits="userSpaceOnUse" xlink:href="#cob"/><linearGradient id="cok" x1="-23.39" x2="23.931" y1="-57.289" y2="8.573" gradientTransform="scale(1.0429 .95884)" gradientUnits="userSpaceOnUse" xlink:href="#coc"/><linearGradient id="col" x1="-53.331" x2="6.771" y1="-30.517" y2="18.785" gradientTransform="scale(.99898 1.001)" gradientUnits="userSpaceOnUse" xlink:href="#cod"/><linearGradient id="com" x1="-14.029" x2="41.998" y1="-23.111" y2="26.259" gradientTransform="scale(1.0003 .99965)" gradientUnits="userSpaceOnUse" xlink:href="#coe"/><linearGradient id="con" x1="31.177" x2="3.37" y1="41.442" y2="3.402" gradientTransform="scale(.96254 1.0389)" gradientUnits="userSpaceOnUse" xlink:href="#cof"/><linearGradient id="coo" x1="-31.905" x2="19.599" y1="-14.258" y2="42.767" gradientTransform="scale(.95823 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#cog"/><linearGradient id="cop" x1="4.301" x2="34.534" y1="34.41" y2="4.514" gradientTransform="scale(1.002 .99796)" gradientUnits="userSpaceOnUse" xlink:href="#coh"/><linearGradient id="coh" x1=".112" x2=".901" y1=".897" y2=".116"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".53"/><stop stop-color="#CD0F7E" offset=".79"/><stop stop-color="#ED2C89" offset="1"/></linearGradient></defs><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#f06292" fill-rule="nonzero"/><g transform="matrix(.31022 .0619 -.0619 .31022 11.807 7.546)" fill="none"><path d="M8.002 6.127L4.117 8.719.116 2.723 4 .13z" transform="rotate(-11.284 17.839 -78.732)" fill="url(#coi)"/><path d="M9.179 1.887l6.637 9.946-7.906 5.276-6.637-9.946L.115 5.43 8.02.153z" transform="rotate(-11.284 129.49 -99.884)" fill="url(#coj)"/><path d="M7.3 1.88l1.462 2.189-6.018 4.015L.124 4.16l1.315-.877L6.143.144z" transform="rotate(-11.284 167.2 -62.32)" fill="url(#cok)"/><path d="M2.328 1.146L4.016.02l2.619 3.925L2.75 6.537 1.29 4.347l2.197-1.466zm-1.04 3.201L.132 2.612l2.197-1.466 1.158 1.735z" transform="rotate(-11.284 104.37 -149.22)" fill="url(#col)"/><path d="M5.346 9.155l-1.315.877L.03 4.035 6.047.019l2.805 4.204L4.15 7.36l4.703-3.138 1.197 1.793z" transform="rotate(-11.284 81.819 7.645)" fill="url(#com)"/><path d="M14.533 9.934l1.197 1.793-7.907 5.276-1.196-1.793L.052 5.358 7.958.082z" transform="rotate(-11.284 17.141 -7.825)" fill="url(#con)"/><path d="M6.235 7.177L4.038 8.643 2.84 6.849.036 2.646 3.92.053 7.923 6.05z" transform="rotate(-11.284 18.188 -79.174)" fill="url(#coo)"/><path d="M18.955 35.925L17.48 34.45l3.998-3.998 1.475 1.475z" fill="#714896"/><path d="M33.33 21.55l-1.475-1.474 1.867-1.868 1.475 1.475z" fill="#6f4795"/><path d="M7.12 24.09l-1.525-1.525 3.998-3.998 1.525 1.525z" fill="#88519f"/><path d="M21.495 9.714L19.97 8.19l1.868-1.868 1.524 1.525z" fill="#85509e"/><path d="M31.418 23.462l-6.72 6.72-1.475-1.474 6.72-6.721z" fill="#8d166a"/><path d="M18.058 10.101l1.525 1.525-6.721 6.72-1.525-1.524z" fill="#a70d6f"/><path d="M2.375 11.769l1.9 1.9-1.9 1.901-1.901-1.9z" fill="#9e61ad"/><path d="M15.523 36.482l1.9 1.9-1.9 1.901-1.9-1.9z" fill="#8053a3"/><path d="M8.372 38.294L.017 29.876 29.749.08l8.636 8.201z" transform="translate(1.823 1.548)" fill="url(#cop)"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-aurelia-open" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="cpi" x1="-31.824" x2="19.682" y1="-11.741" y2="35.548" gradientTransform="scale(.95818 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#cpa"/><linearGradient id="cpa" x1="-3.881" x2="2.377" y1="-1.442" y2="4.304"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpj" x1="12.022" x2="-15.716" y1="13.922" y2="-23.952" gradientTransform="scale(.96226 1.0392)" gradientUnits="userSpaceOnUse" xlink:href="#cpb"/><linearGradient id="cpb" x1=".729" x2="-.971" y1=".844" y2="-1.477"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="cpk" x1="-23.39" x2="23.931" y1="-57.289" y2="8.573" gradientTransform="scale(1.0429 .95884)" gradientUnits="userSpaceOnUse" xlink:href="#cpc"/><linearGradient id="cpc" x1="-2.839" x2="2.875" y1="-6.936" y2="1.017"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpl" x1="-53.331" x2="6.771" y1="-30.517" y2="18.785" gradientTransform="scale(.99898 1.001)" gradientUnits="userSpaceOnUse" xlink:href="#cpd"/><linearGradient id="cpd" x1="-8.212" x2="1.02" y1="-4.691" y2="2.882"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpm" x1="-14.029" x2="41.998" y1="-23.111" y2="26.259" gradientTransform="scale(1.0003 .99965)" gradientUnits="userSpaceOnUse" xlink:href="#cpe"/><linearGradient id="cpe" x1="-1.404" x2="4.19" y1="-2.309" y2="2.62"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpn" x1="31.177" x2="3.37" y1="41.442" y2="3.402" gradientTransform="scale(.96254 1.0389)" gradientUnits="userSpaceOnUse" xlink:href="#cpf"/><linearGradient id="cpf" x1="1.911" x2=".204" y1="2.539" y2=".204"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".29"/><stop stop-color="#CD0F7E" offset=".84"/><stop stop-color="#ED2C89" offset="1"/></linearGradient><linearGradient id="cpo" x1="-31.905" x2="19.599" y1="-14.258" y2="42.767" gradientTransform="scale(.95823 1.0436)" gradientUnits="userSpaceOnUse" xlink:href="#cpg"/><linearGradient id="cpg" x1="-3.881" x2="2.377" y1="-1.738" y2="5.19"><stop stop-color="#C06FBB" offset="0"/><stop stop-color="#6E4D9B" offset="1"/></linearGradient><linearGradient id="cpp" x1="4.301" x2="34.534" y1="34.41" y2="4.514" gradientTransform="scale(1.002 .99796)" gradientUnits="userSpaceOnUse" xlink:href="#cph"/><linearGradient id="cph" x1=".112" x2=".901" y1=".897" y2=".116"><stop stop-color="#6E4D9B" offset="0"/><stop stop-color="#77327A" offset=".14"/><stop stop-color="#B31777" offset=".53"/><stop stop-color="#CD0F7E" offset=".79"/><stop stop-color="#ED2C89" offset="1"/></linearGradient></defs><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#f06292" fill-rule="nonzero"/><g transform="matrix(.31022 .0619 -.0619 .31022 11.807 7.546)" fill="none"><path d="M8.002 6.127L4.117 8.719.116 2.723 4 .13z" transform="rotate(-11.284 17.839 -78.732)" fill="url(#cpi)"/><path d="M9.179 1.887l6.637 9.946-7.906 5.276-6.637-9.946L.115 5.43 8.02.153z" transform="rotate(-11.284 129.49 -99.884)" fill="url(#cpj)"/><path d="M7.3 1.88l1.462 2.189-6.018 4.015L.124 4.16l1.315-.877L6.143.144z" transform="rotate(-11.284 167.2 -62.32)" fill="url(#cpk)"/><path d="M2.328 1.146L4.016.02l2.619 3.925L2.75 6.537 1.29 4.347l2.197-1.466zm-1.04 3.201L.132 2.612l2.197-1.466 1.158 1.735z" transform="rotate(-11.284 104.37 -149.22)" fill="url(#cpl)"/><path d="M5.346 9.155l-1.315.877L.03 4.035 6.047.019l2.805 4.204L4.15 7.36l4.703-3.138 1.197 1.793z" transform="rotate(-11.284 81.819 7.645)" fill="url(#cpm)"/><path d="M14.533 9.934l1.197 1.793-7.907 5.276-1.196-1.793L.052 5.358 7.958.082z" transform="rotate(-11.284 17.141 -7.825)" fill="url(#cpn)"/><path d="M6.235 7.177L4.038 8.643 2.84 6.849.036 2.646 3.92.053 7.923 6.05z" transform="rotate(-11.284 18.188 -79.174)" fill="url(#cpo)"/><path d="M18.955 35.925L17.48 34.45l3.998-3.998 1.475 1.475z" fill="#714896"/><path d="M33.33 21.55l-1.475-1.474 1.867-1.868 1.475 1.475z" fill="#6f4795"/><path d="M7.12 24.09l-1.525-1.525 3.998-3.998 1.525 1.525z" fill="#88519f"/><path d="M21.495 9.714L19.97 8.19l1.868-1.868 1.524 1.525z" fill="#85509e"/><path d="M31.418 23.462l-6.72 6.72-1.475-1.474 6.72-6.721z" fill="#8d166a"/><path d="M18.058 10.101l1.525 1.525-6.721 6.72-1.525-1.524z" fill="#a70d6f"/><path d="M2.375 11.769l1.9 1.9-1.9 1.901-1.901-1.9z" fill="#9e61ad"/><path d="M15.523 36.482l1.9 1.9-1.9 1.901-1.9-1.9z" fill="#8053a3"/><path d="M8.372 38.294L.017 29.876 29.749.08l8.636 8.201z" transform="translate(1.823 1.548)" fill="url(#cpp)"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-components" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#cddc39" fill-rule="nonzero"/><path d="M11.185 9.613h5.346v2.9l3.782-3.775 3.775 3.775-3.775 3.782h2.9v5.346h-5.346v-5.346h2.446l-3.782-3.782v2.446h-5.346V9.613m0 6.682h5.346v5.346h-5.346z" fill="#f0f4c3" stroke-width=".668"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-components-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#cddc39"/><path d="M11.185 9.613h5.346v2.9l3.782-3.775 3.775 3.775-3.775 3.782h2.9v5.346h-5.346v-5.346h2.446l-3.782-3.782v2.446h-5.346V9.613m0 6.682h5.346v5.346h-5.346z" fill="#f0f4c3" stroke-width=".668"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-config" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#00acc1" fill-rule="nonzero"/><path d="M17.293 17.786a2.308 2.308 0 0 1-2.308-2.308 2.308 2.308 0 0 1 2.308-2.307 2.308 2.308 0 0 1 2.308 2.307 2.308 2.308 0 0 1-2.308 2.308m4.899-1.668c.026-.211.046-.422.046-.64 0-.217-.02-.435-.046-.659l1.391-1.075a.333.333 0 0 0 .08-.422l-1.32-2.28c-.079-.146-.257-.205-.402-.146l-1.641.66a4.779 4.779 0 0 0-1.115-.647l-.244-1.747a.333.333 0 0 0-.33-.277h-2.637a.333.333 0 0 0-.33.277l-.243 1.747a4.78 4.78 0 0 0-1.114.646l-1.642-.659a.324.324 0 0 0-.402.145l-1.319 2.281a.325.325 0 0 0 .08.422l1.39 1.075c-.026.224-.046.442-.046.66s.02.428.046.639l-1.39 1.094a.325.325 0 0 0-.08.422l1.319 2.282c.079.145.257.197.402.145l1.642-.666c.342.264.698.488 1.114.653l.244 1.747a.333.333 0 0 0 .33.277h2.637a.333.333 0 0 0 .33-.277l.243-1.747a4.802 4.802 0 0 0 1.115-.653l1.641.666c.145.052.323 0 .403-.145l1.318-2.282a.333.333 0 0 0-.079-.422z" fill="#80deea" stroke-width=".659"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-config-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#00acc1"/><path d="M17.293 17.786a2.308 2.308 0 0 1-2.308-2.308 2.308 2.308 0 0 1 2.308-2.307 2.308 2.308 0 0 1 2.308 2.307 2.308 2.308 0 0 1-2.308 2.308m4.899-1.668c.026-.211.046-.422.046-.64 0-.217-.02-.435-.046-.659l1.391-1.075a.333.333 0 0 0 .08-.422l-1.32-2.28c-.079-.146-.257-.205-.402-.146l-1.641.66a4.779 4.779 0 0 0-1.115-.647l-.244-1.747a.333.333 0 0 0-.33-.277h-2.637a.333.333 0 0 0-.33.277l-.243 1.747a4.78 4.78 0 0 0-1.114.646l-1.642-.659a.324.324 0 0 0-.402.145l-1.319 2.281a.325.325 0 0 0 .08.422l1.39 1.075c-.026.224-.046.442-.046.66s.02.428.046.639l-1.39 1.094a.325.325 0 0 0-.08.422l1.319 2.282c.079.145.257.197.402.145l1.642-.666c.342.264.698.488 1.114.653l.244 1.747a.333.333 0 0 0 .33.277h2.637a.333.333 0 0 0 .33-.277l.243-1.747a4.802 4.802 0 0 0 1.115-.653l1.641.666c.145.052.323 0 .403-.145l1.318-2.282a.333.333 0 0 0-.079-.422z" fill="#80deea" stroke-width=".659"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-css" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#42a5f5" fill-rule="nonzero"/><path d="M12.488 9.415l-.44 2.259h9.188l-.298 1.46h-9.18l-.447 2.251H20.5l-.514 2.576-3.705 1.224-3.211-1.224.223-1.109h-2.258l-.534 2.704 5.307 2.029 6.118-2.029.812-4.076.162-.818 1.041-5.247H12.488z" fill-rule="nonzero" fill="#bbdefb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-css-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#42a5f5" fill-rule="nonzero"/><path d="M12.488 9.415l-.44 2.259h9.188l-.298 1.46h-9.18l-.447 2.251H20.5l-.514 2.576-3.705 1.224-3.211-1.224.223-1.109h-2.258l-.534 2.704 5.307 2.029 6.118-2.029.812-4.076.162-.818 1.041-5.247H12.488z" fill-rule="nonzero" fill="#bbdefb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-dist" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#e57373" fill-rule="nonzero"/><path d="M18.575 11.113h-2.576V9.825h2.576m3.864 1.288h-2.576V9.825l-1.288-1.288h-2.576L14.71 9.825v1.288h-2.577c-.715 0-1.288.573-1.288 1.288v7.085a1.288 1.288 0 0 0 1.288 1.288H22.44a1.288 1.288 0 0 0 1.288-1.288V12.4c0-.715-.58-1.288-1.288-1.288z" fill="#ffcdd2" stroke-width=".644"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-dist-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#e57373"/><path d="M18.575 11.113h-2.576V9.825h2.576m3.864 1.288h-2.576V9.825l-1.288-1.288h-2.576L14.71 9.825v1.288h-2.577c-.715 0-1.288.573-1.288 1.288v7.085a1.288 1.288 0 0 0 1.288 1.288H22.44a1.288 1.288 0 0 0 1.288-1.288V12.4c0-.715-.58-1.288-1.288-1.288z" fill="#ffcdd2" fill-rule="evenodd" stroke-width=".644"/></symbol><symbol id="folder-docker" clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><defs id="cydefs10"><path id="cySVGID_2_" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2 2.1.9 2.1 2-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></defs><path id="cypath2" d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#039be5" fill-rule="nonzero"/><style id="cystyle2">.cyst0{fill:#fff}.cyst1{clip-path:url(#cySVGID_4_)}</style><g id="cyg34" transform="translate(8.319 9.626) scale(.39491)" fill="#b3e5fc"><g id="cyg32"><g id="cyg30"><title id="cytitle4">Group 3</title><g id="cyg28"><g id="cyg26"><g id="cyg9"><path id="cySVGID_1_" class="cyst0" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2 2.1.9 2.1 2-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/></g><g id="cyg24"><clipPath id="cySVGID_4_"><use id="cyuse14" width="100%" height="100%" xlink:href="#cySVGID_2_"/></clipPath><g id="cyg22" class="cyst1" clip-path="url(#cySVGID_4_)"><g id="cyg20"><g id="cyg18"><path id="cySVGID_3_" class="cyst0" d="M-48.8-21H1226v151.4H-48.8z"/></g></g></g></g></g></g></g></g></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-docker-open" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="cza"><use width="100%" height="100%" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#SVGID_2_"/></clipPath></defs><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#039be5"/><g transform="matrix(.3949 0 0 .39489 8.319 9.626)" fill="#b3e5fc"><title>Group 3</title><path class="czst0" d="M8.7 24c-1.1 0-2.1-.9-2.1-2s.9-2 2.1-2 2.1.9 2.1 2-1 2-2.1 2zm25.8-10.9c-.2-1.6-1.2-2.9-2.5-3.9l-.5-.4-.4.5c-.8.9-1.1 2.5-1 3.7.1.9.4 1.8.9 2.5-.4.2-.9.4-1.3.6-.9.3-1.8.4-2.7.4H1.1l-.1.6c-.2 1.9.1 3.9.9 5.7l.4.7v.1c2.4 4 6.7 5.8 11.4 5.8 9 0 16.4-3.9 19.9-12.3 2.3.1 4.6-.5 5.7-2.7l.3-.5-.5-.3c-1.3-.8-3.1-.9-4.6-.5zm-12.9-1.6h-3.9v3.9h3.9zm0-4.9h-3.9v3.9h3.9zm0-5h-3.9v3.9h3.9zm4.8 9.9h-3.9v3.9h3.9zm-14.5 0H8v3.9h3.9zm4.9 0h-3.9v3.9h3.9zm-9.7 0H3.2v3.9h3.9zm9.7-4.9h-3.9v3.9h3.9zm-4.9 0H8v3.9h3.9z"/><g class="czst1" clip-path="url(#cza)"><path class="czst0" d="M-48.8-21H1226v151.4H-48.8z"/></g></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-docs" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#0277bd" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m6.075 10.8v-1.35H13.85v1.35h6.075m2.025-2.7v-1.35h-8.1v1.35h8.1z" fill-rule="nonzero" fill="#b3e5fc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-docs-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#0277bd" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m6.075 10.8v-1.35H13.85v1.35h6.075m2.025-2.7v-1.35h-8.1v1.35h8.1z" fill-rule="nonzero" fill="#b3e5fc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-expo" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#01579b" fill-rule="nonzero"/><style>.dcst0{fill:#1173b6}.st1{fill:#585d67}</style><path class="dcst0" d="M18.575 9.82c-.489-.745-.605-.844-1.6-.844h-.024c-.996 0-1.106.099-1.601.844-.46.699-5.024 9.058-5.024 9.291 0 .338.087.658.402 1.112.32.46.873.716 1.275.309.273-.274 3.201-5.321 4.616-7.23a.425.425 0 0 1 .693 0c1.414 1.909 4.343 6.956 4.616 7.23.402.407.955.15 1.275-.309.314-.454.402-.774.402-1.112-.006-.233-4.57-8.598-5.03-9.291z" fill="#1173b6" stroke-width=".058"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-expo-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#01579b"/><path class="ddst0" d="M18.575 9.82c-.489-.745-.605-.844-1.6-.844h-.024c-.996 0-1.106.099-1.601.844-.46.699-5.024 9.058-5.024 9.291 0 .338.087.658.402 1.112.32.46.873.716 1.275.309.273-.274 3.201-5.321 4.616-7.23a.425.425 0 0 1 .693 0c1.414 1.909 4.343 6.956 4.616 7.23.402.407.955.15 1.275-.309.314-.454.402-.774.402-1.112-.006-.233-4.57-8.598-5.03-9.291z" fill="#1173b6" stroke-width=".058" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-font" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ef9a9a" fill-rule="nonzero"/><path d="M14.62 17.403l2.38-6.33 2.37 6.33m-3.37-9l-5.5 14h2.25l1.12-3h6.25l1.13 3h2.25l-5.5-14h-2z" fill="#f44336" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-font-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ef9a9a" fill-rule="nonzero"/><path d="M14.62 17.403l2.38-6.33 2.37 6.33m-3.37-9l-5.5 14h2.25l1.12-3h6.25l1.13 3h2.25l-5.5-14h-2z" fill="#f44336" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-git" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ff8a65" fill-rule="nonzero"/><path d="M10.43 14.14l4.044-4.052 1.183 1.19a1.387 1.387 0 0 0 .65 1.56v3.877c-.42.238-.699.693-.699 1.21 0 .768.632 1.4 1.4 1.4.767 0 1.4-.632 1.4-1.4 0-.517-.28-.972-.7-1.21v-3.4l1.448 1.462c-.05.105-.05.224-.05.35 0 .767.633 1.399 1.4 1.399.768 0 1.4-.632 1.4-1.4 0-.767-.632-1.4-1.4-1.4-.126 0-.245 0-.35.05l-1.798-1.799a1.385 1.385 0 0 0-.805-1.637c-.3-.112-.615-.14-.895-.063l-1.19-1.183.553-.545a1.381 1.381 0 0 1 1.973 0l5.591 5.59a1.381 1.381 0 0 1 0 1.974l-5.59 5.591a1.381 1.381 0 0 1-1.974 0l-5.591-5.59a1.381 1.381 0 0 1 0-1.974z" fill="#e64a19" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-git-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ff8a65" fill-rule="nonzero"/><path d="M10.43 14.14l4.044-4.052 1.183 1.19a1.387 1.387 0 0 0 .65 1.56v3.877c-.42.238-.699.693-.699 1.21 0 .768.632 1.4 1.4 1.4.767 0 1.4-.632 1.4-1.4 0-.517-.28-.972-.7-1.21v-3.4l1.448 1.462c-.05.105-.05.224-.05.35 0 .767.633 1.399 1.4 1.399.768 0 1.4-.632 1.4-1.4 0-.767-.632-1.4-1.4-1.4-.126 0-.245 0-.35.05l-1.798-1.799a1.385 1.385 0 0 0-.805-1.637c-.3-.112-.615-.14-.895-.063l-1.19-1.183.553-.545a1.381 1.381 0 0 1 1.973 0l5.591 5.59a1.381 1.381 0 0 1 0 1.974l-5.59 5.591a1.381 1.381 0 0 1-1.974 0l-5.591-5.59a1.381 1.381 0 0 1 0-1.974z" fill="#e64a19" fill-rule="nonzero"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-global" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#5c6bc0" fill-rule="nonzero"/><path d="M21.132 18.585a1.22 1.22 0 0 0-1.156-.846h-.609v-1.825a.608.608 0 0 0-.608-.609h-3.65v-1.217h1.216a.608.608 0 0 0 .609-.608v-1.217h1.217a1.217 1.217 0 0 0 1.216-1.217v-.25a4.858 4.858 0 0 1 1.765 7.79m-4.198 1.545a4.86 4.86 0 0 1-4.26-4.826c0-.377.049-.742.128-1.089l2.915 2.915v.608a1.217 1.217 0 0 0 1.217 1.217m.608-9.735a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.085-6.085 6.085 6.085 0 0 0-6.085-6.084z" fill="#c5cae9" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-global-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#5c6bc0"/><path d="M21.133 18.585a1.22 1.22 0 0 0-1.156-.846h-.609v-1.825a.608.608 0 0 0-.608-.609h-3.65v-1.217h1.216a.608.608 0 0 0 .609-.608v-1.217h1.217a1.217 1.217 0 0 0 1.216-1.217v-.25a4.858 4.858 0 0 1 1.765 7.79m-4.198 1.545a4.86 4.86 0 0 1-4.26-4.826c0-.377.049-.742.128-1.089l2.915 2.915v.608a1.217 1.217 0 0 0 1.216 1.217m.609-9.735a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.085-6.085 6.085 6.085 0 0 0-6.085-6.084z" fill="#c5cae9" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-i18n" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#5c6bc0" fill-rule="nonzero"/><path d="M17.293 17.786l-1.53-1.512.018-.018a10.555 10.555 0 0 0 2.235-3.934h1.765v-1.205h-4.217V9.912h-1.205v1.205h-4.217v1.205h6.73a9.5 9.5 0 0 1-1.91 3.223 9.424 9.424 0 0 1-1.392-2.018h-1.205c.44.982 1.042 1.91 1.795 2.747l-3.067 3.024.856.856 3.012-3.013 1.874 1.874.458-1.229m3.392-3.054H19.48l-2.711 7.23h1.205l.674-1.808h2.862l.68 1.807h1.206l-2.711-7.23m-1.579 4.218l.976-2.609.976 2.609z" fill="#c5cae9" stroke-width=".602"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-i18n-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#5c6bc0"/><path d="M17.293 17.786l-1.53-1.512.018-.018a10.555 10.555 0 0 0 2.235-3.934h1.765v-1.205h-4.217V9.912h-1.205v1.205h-4.217v1.205h6.73a9.5 9.5 0 0 1-1.91 3.223 9.424 9.424 0 0 1-1.392-2.018h-1.205c.44.982 1.042 1.91 1.795 2.747l-3.067 3.024.856.856 3.012-3.013 1.874 1.874.458-1.229m3.392-3.054H19.48l-2.711 7.23h1.205l.674-1.808h2.862l.68 1.807h1.206l-2.711-7.23m-1.579 4.218l.976-2.609.976 2.609z" fill="#c5cae9" stroke-width=".602"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-images" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#009688" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m0 12.15h8.1v-5.4l-2.7 2.7-1.35-1.35-4.05 4.05m1.35-7.425c-.74 0-1.35.61-1.35 1.35s.61 1.35 1.35 1.35 1.35-.61 1.35-1.35-.61-1.35-1.35-1.35z" fill-rule="nonzero" fill="#b2dfdb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-images-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#009688" fill-rule="nonzero"/><path d="M18.575 12.859h3.713l-3.713-3.713v3.713M13.85 8.134h5.4l4.05 4.05v8.1c0 .74-.61 1.35-1.35 1.35h-8.1a1.35 1.35 0 0 1-1.35-1.35v-10.8c0-.75.6-1.35 1.35-1.35m0 12.15h8.1v-5.4l-2.7 2.7-1.35-1.35-4.05 4.05m1.35-7.425c-.74 0-1.35.61-1.35 1.35s.61 1.35 1.35 1.35 1.35-.61 1.35-1.35-.61-1.35-1.35-1.35z" fill-rule="nonzero" fill="#b2dfdb"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-include" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#039be5" fill-rule="nonzero"/><path d="M20.788 15.981h-2.434v2.434h-1.217V15.98h-2.434v-1.217h2.434V12.33h1.217v2.434h2.434m-3.042-5.476a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.084-6.085 6.085 6.085 0 0 0-6.084-6.084z" fill="#b3e5fc" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-include-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#039be5"/><path d="M20.788 15.981h-2.434v2.434h-1.217V15.98h-2.434v-1.217h2.434V12.33h1.217v2.434h2.434m-3.042-5.476a6.085 6.085 0 0 0-6.085 6.084 6.085 6.085 0 0 0 6.085 6.085 6.085 6.085 0 0 0 6.084-6.085 6.085 6.085 0 0 0-6.084-6.084z" fill="#b3e5fc" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-javascript" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ffca28" fill-rule="nonzero"/><path d="M17.935 18.374a2.18 2.18 0 0 0 1.972 1.213c.829 0 1.354-.415 1.354-.987 0-.682-.542-.927-1.452-1.324l-.502-.216c-1.435-.613-2.404-1.378-2.404-3.005 0-1.5 1.167-2.638 2.917-2.638a2.957 2.957 0 0 1 2.842 1.599l-1.552.999a1.362 1.362 0 0 0-1.29-.858.873.873 0 0 0-.957.858c0 .583.374.84 1.226 1.213l.502.216c1.697.733 2.654 1.47 2.654 3.139 0 1.798-1.411 2.783-3.308 2.783a3.839 3.839 0 0 1-3.618-2.046zm-7.048.175c.315.583.583 1.027 1.283 1.027s1.066-.256 1.066-1.255v-6.774h1.998v6.804c0 2.064-1.214 3.01-2.982 3.01a3.104 3.104 0 0 1-2.993-1.826z" fill-rule="nonzero" fill="#ffecb3"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-javascript-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ffca28"/><path d="M17.935 18.374a2.18 2.18 0 0 0 1.972 1.213c.829 0 1.354-.415 1.354-.987 0-.682-.542-.927-1.452-1.324l-.502-.216c-1.435-.613-2.404-1.378-2.404-3.005 0-1.5 1.167-2.638 2.917-2.638a2.957 2.957 0 0 1 2.842 1.599l-1.552.999a1.362 1.362 0 0 0-1.29-.858.873.873 0 0 0-.957.858c0 .583.374.84 1.226 1.213l.502.216c1.697.733 2.654 1.47 2.654 3.139 0 1.798-1.412 2.783-3.308 2.783a3.839 3.839 0 0 1-3.618-2.046zm-7.048.175c.315.583.583 1.027 1.283 1.027s1.066-.256 1.066-1.255v-6.774h1.998v6.804c0 2.064-1.214 3.01-2.982 3.01a3.104 3.104 0 0 1-2.993-1.826z" fill="#ffecb3"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-lib" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#c0ca33" fill-rule="nonzero"/><path d="M17.39 12.544a2.05 2.05 0 0 0 2.05-2.05 2.05 2.05 0 0 0-2.05-2.052 2.05 2.05 0 0 0-2.05 2.051 2.05 2.05 0 0 0 2.05 2.051m0 2.42a8.992 8.992 0 0 0-6.152-2.42v7.52c2.392 0 4.539.923 6.152 2.42a8.992 8.992 0 0 1 6.152-2.42v-7.52c-2.392 0-4.539.923-6.152 2.42z" fill="#f0f4c3" stroke-width=".684"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-lib-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#c0ca33"/><path d="M17.391 12.543a2.05 2.05 0 0 0 2.05-2.05 2.05 2.05 0 0 0-2.05-2.052 2.05 2.05 0 0 0-2.05 2.051 2.05 2.05 0 0 0 2.05 2.051m0 2.42a8.992 8.992 0 0 0-6.152-2.42v7.52c2.392 0 4.539.923 6.152 2.42a8.992 8.992 0 0 1 6.152-2.42v-7.52c-2.392 0-4.539.923-6.152 2.42z" fill="#f0f4c3" stroke-width=".684"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-actions" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ab47bc" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#e1bee7" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-actions-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ab47bc"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#e1bee7" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-effects" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#00bcd4" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#b2ebf2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-effects-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#00bcd4"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#b2ebf2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-reducer" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ef5350" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#ffcdd2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-reducer-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ef5350"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#ffcdd2" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-state" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#8bc34a" fill-rule="nonzero"/><path d="M17.655 8.39l-6.152 2.193.933 8.142 5.219 2.888 5.219-2.888.932-8.142zm-1.278 2.067c.234-.004.487.07.768.223.124.066.498.16.83.21 1.183.17 2.586 1.073 3.03 1.95.306.602.243.927-.225 1.169-.404.209-1.23.108-2.43-.297l-1.012-.342-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.518 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.004-.363-.046 0-.04.164-.243.363-.451.748-.781 1.004-1.365 1.083-2.474l.055-.767.176.365c.194.401.23.98.091 1.478-.115.416-.038.462.173.104.261-.443.345-.373.299.251-.05.678-.283 1.187-.808 1.762-.429.468-.377.552.141.233.5-.308.567-.26.31.224-.487.914-1.516 1.69-2.585 1.948-.647.158-1.106.187-1.7.11-1.55-.204-3.018-1.249-3.718-2.648a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.141.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.108.282-.199.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#dcedc8" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-ngrx-state-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#8bc34a"/><path d="M17.655 8.39l-6.152 2.192.933 8.143 5.219 2.888 5.219-2.888.932-8.143zm-1.278 2.067c.234-.004.487.07.768.222.124.067.498.162.83.21 1.183.171 2.586 1.074 3.03 1.95.306.603.243.928-.225 1.17-.404.208-1.23.107-2.43-.298l-1.012-.341-.36.137c-.522.2-1.044.694-1.258 1.19-.154.359-.177.527-.149 1.116.028.59.071.761.28 1.132.239.422.786.96.88.866.026-.026-.03-.197-.124-.38-.093-.183-.148-.368-.122-.41.026-.042.273.114.548.347.611.517 1.326.848 1.981.917.538.056.661-.044.258-.211a1.238 1.238 0 0 1-.374-.25c-.157-.173-.166-.168.504-.318.417-.094 1.24-.531 1.29-.685.016-.05-.118-.07-.338-.053-.2.016-.363-.005-.363-.046 0-.04.164-.243.363-.452.748-.78 1.004-1.364 1.083-2.474l.055-.766.176.364c.194.402.23.981.091 1.479-.115.416-.038.462.173.103.261-.442.345-.372.299.252-.05.678-.283 1.186-.808 1.761-.429.47-.377.553.141.234.5-.308.567-.26.31.224-.487.914-1.516 1.689-2.585 1.948-.647.158-1.106.187-1.7.109-1.55-.203-3.018-1.248-3.718-2.647a8.736 8.736 0 0 0-.572-.989c-.275-.373-.298-.54-.113-.823.093-.142.114-.286.076-.502-.176-.999-.17-1.03.23-1.437.35-.353.371-.4.371-.813 0-.358.036-.475.198-.637.109-.109.282-.2.384-.2.296-.003.807-.277 1.11-.595.252-.265.52-.4.822-.404z" fill="#dcedc8" stroke-width=".696"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-node" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#8bc34a" fill-rule="nonzero"/><path d="M17.25 8.403c-.188 0-.382.048-.542.139l-5.166 2.986a1.096 1.096 0 0 0-.542.944v5.959c0 .388.208.75.542.944l1.354.778c.66.32.882.326 1.187.326.973 0 1.535-.59 1.535-1.618V12.98a.154.154 0 0 0-.153-.153h-.646c-.09 0-.16.07-.16.153v5.882c0 .458-.472.91-1.228.528l-1.424-.813a.181.181 0 0 1-.076-.145v-5.959c0-.062.027-.118.076-.146l5.167-2.979a.15.15 0 0 1 .152 0l5.167 2.98a.164.164 0 0 1 .076.145v5.959a.181.181 0 0 1-.076.145l-5.167 2.98c-.041.027-.11.027-.16 0l-1.305-.792c-.055-.021-.111-.028-.146-.007-.368.208-.437.25-.778.354-.083.028-.215.076.05.222l1.721 1.021c.167.097.348.146.542.146s.375-.049.542-.146l5.166-2.979c.334-.194.542-.556.542-.944v-5.959c0-.389-.208-.75-.542-.944l-5.166-2.986a1.103 1.103 0 0 0-.542-.14m1.389 4.272c-1.472 0-2.354.618-2.354 1.66 0 1.117.875 1.444 2.291 1.583 1.688.166 1.82.416 1.82.75 0 .576-.465.82-1.549.82-1.375 0-1.666-.341-1.77-1.022a.157.157 0 0 0-.153-.125h-.667c-.083 0-.146.063-.146.153 0 .861.472 1.903 2.736 1.903 1.632 0 2.57-.646 2.57-1.771 0-1.118-.75-1.41-2.34-1.625-1.605-.208-1.765-.32-1.765-.694 0-.313.14-.73 1.327-.73 1.042 0 1.451.23 1.611.945.014.07.076.118.146.118h.673a.134.134 0 0 0 .105-.049c.027-.028.048-.07.034-.11-.097-1.237-.916-1.806-2.57-1.806z" fill-rule="nonzero" fill="#f1f8e9"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-node-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#8bc34a" fill-rule="nonzero"/><path d="M17.25 8.403c-.188 0-.382.048-.542.139l-5.166 2.986a1.096 1.096 0 0 0-.542.944v5.959c0 .388.208.75.542.944l1.354.778c.66.32.882.326 1.187.326.973 0 1.535-.59 1.535-1.618V12.98a.154.154 0 0 0-.153-.153h-.646c-.09 0-.16.07-.16.153v5.882c0 .458-.472.91-1.228.528l-1.424-.813a.181.181 0 0 1-.076-.145v-5.959c0-.062.027-.118.076-.146l5.167-2.979a.15.15 0 0 1 .152 0l5.167 2.98a.164.164 0 0 1 .076.145v5.959a.181.181 0 0 1-.076.145l-5.167 2.98c-.041.027-.11.027-.16 0l-1.305-.792c-.055-.021-.111-.028-.146-.007-.368.208-.437.25-.778.354-.083.028-.215.076.05.222l1.721 1.021c.167.097.348.146.542.146s.375-.049.542-.146l5.166-2.979c.334-.194.542-.556.542-.944v-5.959c0-.389-.208-.75-.542-.944l-5.166-2.986a1.103 1.103 0 0 0-.542-.14m1.389 4.272c-1.472 0-2.354.618-2.354 1.66 0 1.117.875 1.444 2.291 1.583 1.688.166 1.82.416 1.82.75 0 .576-.465.82-1.549.82-1.375 0-1.666-.341-1.77-1.022a.157.157 0 0 0-.153-.125h-.667c-.083 0-.146.063-.146.153 0 .861.472 1.903 2.736 1.903 1.632 0 2.57-.646 2.57-1.771 0-1.118-.75-1.41-2.34-1.625-1.605-.208-1.765-.32-1.765-.694 0-.313.14-.73 1.327-.73 1.042 0 1.451.23 1.611.945.014.07.076.118.146.118h.673a.134.134 0 0 0 .105-.049c.027-.028.048-.07.034-.11-.097-1.237-.916-1.806-2.57-1.806z" fill-rule="nonzero" fill="#f1f8e9"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-public" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#039be5" fill-rule="nonzero"/><path d="M20.036 16.746c.05-.408.087-.817.087-1.237s-.037-.83-.087-1.238h2.091c.099.396.16.81.16 1.238a5.1 5.1 0 0 1-.16 1.237m-3.186 3.44c.371-.687.656-1.43.854-2.203h1.825a4.968 4.968 0 0 1-2.679 2.203m-.155-3.44h-2.895c-.062-.408-.099-.817-.099-1.237s.037-.835.1-1.238h2.894c.056.403.1.817.1 1.238s-.044.829-.1 1.237m-1.447 3.687a8.39 8.39 0 0 1-1.182-2.45h2.363a8.39 8.39 0 0 1-1.181 2.45m-2.475-7.399h-1.806a4.902 4.902 0 0 1 2.672-2.202c-.37.686-.65 1.429-.866 2.202m-1.806 4.95h1.806c.217.773.495 1.515.866 2.202a4.954 4.954 0 0 1-2.672-2.203m-.508-1.237a5.099 5.099 0 0 1-.16-1.237 5.1 5.1 0 0 1 .16-1.238h2.091c-.049.409-.086.817-.086 1.238s.037.829.086 1.237m2.698-6.168a8.425 8.425 0 0 1 1.181 2.456h-2.363a8.426 8.426 0 0 1 1.182-2.456m4.28 2.456h-1.824a9.682 9.682 0 0 0-.854-2.202 4.94 4.94 0 0 1 2.679 2.202m-4.281-3.712a6.193 6.193 0 0 0-6.187 6.187 6.186 6.186 0 0 0 6.187 6.186 6.186 6.186 0 0 0 6.186-6.186 6.186 6.186 0 0 0-6.186-6.187z" fill="#b3e5fc" stroke-width=".619"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-public-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#039be5"/><path d="M20.037 16.746c.05-.408.087-.817.087-1.237s-.037-.83-.087-1.238h2.091c.099.396.16.81.16 1.238a5.1 5.1 0 0 1-.16 1.237m-3.186 3.44c.371-.687.656-1.43.854-2.203h1.825a4.967 4.967 0 0 1-2.68 2.203m-.154-3.44h-2.895c-.062-.408-.099-.817-.099-1.237s.037-.835.099-1.238h2.895c.056.403.1.817.1 1.238s-.044.829-.1 1.237m-1.447 3.687a8.39 8.39 0 0 1-1.182-2.45h2.363a8.39 8.39 0 0 1-1.181 2.45m-2.475-7.399H13.06a4.902 4.902 0 0 1 2.672-2.202c-.371.686-.65 1.429-.866 2.202m-1.806 4.95h1.806c.217.773.495 1.515.866 2.202a4.954 4.954 0 0 1-2.672-2.203m-.508-1.237a5.099 5.099 0 0 1-.16-1.237 5.1 5.1 0 0 1 .16-1.238h2.091c-.05.409-.086.817-.086 1.238s.037.829.086 1.237m2.698-6.168a8.425 8.425 0 0 1 1.181 2.456h-2.363a8.426 8.426 0 0 1 1.182-2.456m4.28 2.456h-1.824a9.682 9.682 0 0 0-.854-2.202 4.941 4.941 0 0 1 2.679 2.202M17.34 9.322a6.193 6.193 0 0 0-6.187 6.187 6.186 6.186 0 0 0 6.187 6.186 6.186 6.186 0 0 0 6.186-6.186 6.186 6.186 0 0 0-6.186-6.187z" fill="#b3e5fc" stroke-width=".619"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-react-components" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#00bcd4" fill-rule="nonzero"/><path d="M16.473 13.927c.723 0 1.313.59 1.313 1.327 0 .703-.59 1.3-1.313 1.3a1.318 1.318 0 0 1-1.313-1.3c0-.737.59-1.327 1.313-1.327m-3.252 6.946c.443.267 1.412-.14 2.529-1.194a17.015 17.015 0 0 1-1.06-1.335 15.945 15.945 0 0 1-1.686-.252c-.358 1.502-.225 2.535.217 2.78m.499-4.03l-.204-.359a5.558 5.558 0 0 0-.203.604c.19.042.4.078.618.113l-.211-.359m4.593-.533l.569-1.054-.569-1.053c-.21-.372-.435-.702-.639-1.032-.38-.022-.78-.022-1.2-.022-.422 0-.823 0-1.202.022-.204.33-.428.66-.639 1.032l-.569 1.053.57 1.054c.21.372.434.702.638 1.032.38.021.78.021 1.201.021.421 0 .822 0 1.201-.02.204-.331.428-.661.639-1.033m-1.84-4.72c-.133.155-.274.316-.414.506h.828c-.14-.19-.28-.351-.414-.506m0 7.332c.133-.154.274-.316.414-.505h-.828c.14.19.28.35.414.505m3.245-9.284c-.436-.267-1.405.14-2.522 1.194.366.414.724.864 1.06 1.334.577.057 1.146.14 1.686.253.359-1.503.225-2.535-.224-2.78m-.492 4.03l.204.358c.077-.203.154-.407.203-.604-.19-.042-.4-.077-.618-.112l.211.358m1.018-4.95c1.033.589 1.145 2.141.71 3.953 1.784.527 3.069 1.398 3.069 2.584 0 1.187-1.285 2.058-3.07 2.585.436 1.812.324 3.364-.709 3.954-1.025.59-2.423-.085-3.77-1.37-1.35 1.285-2.747 1.96-3.78 1.37-1.025-.59-1.137-2.142-.702-3.954-1.783-.527-3.069-1.398-3.069-2.585s1.286-2.057 3.07-2.584c-.436-1.812-.324-3.364.702-3.954 1.032-.59 2.43.084 3.778 1.37 1.348-1.286 2.746-1.96 3.771-1.37m-.203 6.538c.239.527.45 1.054.625 1.588 1.475-.443 2.303-1.075 2.303-1.588 0-.512-.828-1.144-2.303-1.587a15.81 15.81 0 0 1-.625 1.587m-7.136 0a15.806 15.806 0 0 1-.625-1.587c-1.474.443-2.303 1.075-2.303 1.587 0 .513.829 1.145 2.303 1.588.176-.534.387-1.06.625-1.588m6.321 1.588l-.21.358c.217-.035.428-.07.617-.113-.049-.196-.126-.4-.203-.604l-.204.359m-2.03 2.837c1.117 1.053 2.086 1.46 2.522 1.194.45-.246.583-1.278.224-2.781-.54.112-1.11.196-1.685.253-.337.47-.695.92-1.06 1.334m-3.477-6.012l.21-.358c-.217.035-.428.07-.617.113.049.196.126.4.203.604l.204-.359m2.03-2.837c-1.117-1.053-2.086-1.46-2.529-1.194-.442.246-.576 1.278-.217 2.781.54-.112 1.11-.196 1.685-.253.337-.47.695-.92 1.06-1.334z" fill="#b2ebf2" stroke-width=".702"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-react-components-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#00bcd4"/><path d="M16.473 13.928c.723 0 1.313.59 1.313 1.327 0 .703-.59 1.3-1.313 1.3a1.318 1.318 0 0 1-1.313-1.3c0-.737.59-1.327 1.313-1.327m-3.252 6.946c.443.267 1.412-.14 2.529-1.194a16.997 16.997 0 0 1-1.06-1.335 15.945 15.945 0 0 1-1.686-.252c-.358 1.502-.225 2.535.217 2.78m.499-4.03l-.204-.359c-.077.204-.154.408-.203.604.19.042.4.078.618.113l-.211-.359m4.593-.533l.569-1.054-.57-1.053c-.21-.372-.434-.702-.638-1.032-.38-.022-.78-.022-1.201-.022-.421 0-.822 0-1.2.022-.205.33-.43.66-.64 1.032l-.569 1.053.569 1.054c.21.372.435.702.64 1.032.378.021.779.021 1.2.021.421 0 .822 0 1.2-.02.205-.33.43-.661.64-1.033m-1.84-4.72c-.133.155-.274.316-.414.506h.828c-.14-.19-.28-.351-.414-.506m0 7.332c.133-.154.274-.316.414-.505h-.828c.14.19.28.35.414.505m3.244-9.284c-.435-.267-1.404.14-2.52 1.194.364.414.723.864 1.06 1.334.575.057 1.144.14 1.685.253.358-1.503.225-2.535-.225-2.78m-.491 4.03l.203.358c.078-.203.155-.407.204-.604-.19-.042-.4-.077-.618-.112l.21.358m1.02-4.95c1.032.589 1.144 2.141.708 3.953 1.784.527 3.07 1.398 3.07 2.584 0 1.187-1.286 2.058-3.07 2.585.436 1.812.323 3.364-.709 3.954-1.025.59-2.423-.085-3.771-1.37-1.348 1.285-2.746 1.96-3.778 1.37-1.026-.59-1.138-2.142-.703-3.954-1.783-.527-3.069-1.398-3.069-2.585s1.286-2.057 3.07-2.584c-.436-1.812-.324-3.364.702-3.954 1.032-.59 2.43.084 3.778 1.37 1.348-1.286 2.746-1.96 3.771-1.37m-.204 6.538c.24.527.45 1.054.625 1.588 1.475-.443 2.304-1.075 2.304-1.588 0-.512-.829-1.144-2.304-1.587a15.81 15.81 0 0 1-.625 1.587m-7.135 0a15.808 15.808 0 0 1-.625-1.587c-1.475.443-2.303 1.075-2.303 1.587 0 .513.828 1.145 2.303 1.588.176-.534.386-1.06.625-1.588m6.32 1.588l-.21.358c.218-.035.428-.07.618-.113a5.56 5.56 0 0 0-.204-.604l-.203.359m-2.03 2.837c1.117 1.053 2.086 1.46 2.521 1.194.45-.246.583-1.278.225-2.781-.54.112-1.11.196-1.685.253-.338.47-.696.92-1.06 1.334m-3.477-6.012l.21-.358c-.217.035-.428.07-.617.112.049.197.126.4.203.604l.204-.358m2.03-2.837c-1.117-1.053-2.086-1.46-2.529-1.194-.442.246-.576 1.278-.217 2.781.54-.112 1.11-.196 1.685-.253.337-.47.695-.92 1.06-1.334z" fill="#b2ebf2" stroke-width=".702"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-actions" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ab47bc" fill-rule="nonzero"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#e1bee7" stroke="#e1bee7" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-actions-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ab47bc"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#e1bee7" stroke="#e1bee7" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-reducer" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ef5350" fill-rule="nonzero"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#ffcdd2" stroke="#ffcdd2" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-reducer-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ef5350"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#ffcdd2" stroke="#ffcdd2" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-store" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#8bc34a" fill-rule="nonzero"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#dcedc8" stroke="#dcedc8" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-redux-store-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#8bc34a"/><g transform="translate(8.378 6.436) scale(.17228)" fill="#dcedc8" stroke="#dcedc8" stroke-miterlimit="4" stroke-width="1.702"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8S68 54.2 65 54.2h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-resource" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#fbc02d" fill-rule="nonzero"/><path d="M21.598 12.059h-6.085v-1.217h6.085m-2.434 6.085h-3.65V15.71h3.65m2.434-1.217h-6.085v-1.217h6.085m.608-4.26h-7.301a1.217 1.217 0 0 0-1.217 1.218v7.301a1.217 1.217 0 0 0 1.217 1.217h7.301a1.217 1.217 0 0 0 1.217-1.217v-7.301a1.217 1.217 0 0 0-1.217-1.217m-9.735 2.434h-1.217v8.518a1.217 1.217 0 0 0 1.217 1.217h8.519v-1.217H12.47z" fill="#fff9c4" stroke-width=".608"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-resource-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#fbc02d"/><path d="M21.598 12.059h-6.085v-1.217h6.085m-2.434 6.085h-3.65V15.71h3.65m2.434-1.217h-6.085v-1.217h6.085m.608-4.26h-7.301a1.217 1.217 0 0 0-1.217 1.218v7.301a1.217 1.217 0 0 0 1.217 1.217h7.301a1.217 1.217 0 0 0 1.217-1.217v-7.301a1.217 1.217 0 0 0-1.217-1.217m-9.735 2.433h-1.217v8.519a1.217 1.217 0 0 0 1.217 1.217h8.519v-1.217H12.47z" fill="#fff9c4" stroke-width=".608"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" id="folder-sass" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#f8bbd0" fill-rule="nonzero"/><path d="M23.36 10.506c-.39-1.527-2.922-2.03-5.319-1.178-1.426.507-2.97 1.302-4.08 2.34-1.32 1.235-1.53 2.31-1.444 2.759.306 1.584 2.477 2.62 3.37 3.388v.005c-.264.13-2.19 1.104-2.64 2.1-.476 1.052.075 1.806.44 1.908 1.131.315 2.292-.251 2.916-1.182.602-.897.551-2.056.29-2.633.36-.095.781-.138 1.316-.076 1.508.177 1.804 1.118 1.748 1.513-.057.394-.373.61-.48.676-.105.065-.137.088-.129.137.013.07.062.068.152.053.125-.021.792-.321.821-1.048.037-.924-.849-1.958-2.416-1.93-.646.01-1.052.072-1.345.181-.022-.024-.044-.05-.067-.073-.969-1.034-2.76-1.765-2.684-3.156.027-.505.203-1.835 3.442-3.45 2.653-1.322 4.777-.957 5.145-.151.524 1.152-1.136 3.293-3.891 3.601-1.05.118-1.603-.289-1.74-.44-.145-.16-.166-.167-.22-.137-.088.049-.033.19 0 .274.082.214.42.594.995.782.506.166 1.739.258 3.23-.319 1.669-.646 2.972-2.443 2.59-3.944zm-7.103 7.783a2.2 2.2 0 0 1-.065 1.413 2.405 2.405 0 0 1-.453.704c-.5.546-1.198.752-1.497.579-.323-.188-.161-.956.418-1.568.623-.66 1.52-1.083 1.52-1.083l-.002-.002.079-.043z" fill="#ec407a" fill-rule="nonzero" stroke="#ec407a" stroke-width=".5199012000000001"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" id="folder-sass-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#f8bbd0" fill-rule="nonzero"/><path d="M23.36 10.506c-.39-1.527-2.922-2.03-5.319-1.178-1.426.507-2.97 1.302-4.08 2.34-1.32 1.235-1.53 2.31-1.444 2.759.306 1.584 2.477 2.62 3.37 3.388v.005c-.264.13-2.19 1.104-2.64 2.1-.476 1.052.075 1.806.44 1.908 1.131.315 2.292-.251 2.916-1.182.602-.897.551-2.056.29-2.633.36-.095.781-.138 1.316-.076 1.508.177 1.804 1.118 1.748 1.513-.057.394-.373.61-.48.676-.105.065-.137.088-.129.137.013.07.062.068.152.053.125-.021.792-.321.821-1.048.037-.924-.849-1.958-2.416-1.93-.646.01-1.052.072-1.345.181-.022-.024-.044-.05-.067-.073-.969-1.034-2.76-1.765-2.684-3.156.027-.505.203-1.835 3.442-3.45 2.653-1.322 4.777-.957 5.145-.151.524 1.152-1.136 3.293-3.891 3.601-1.05.118-1.603-.289-1.74-.44-.145-.16-.166-.167-.22-.137-.088.049-.033.19 0 .274.082.214.42.594.995.782.506.166 1.739.258 3.23-.319 1.669-.646 2.972-2.443 2.59-3.944zm-7.103 7.783a2.2 2.2 0 0 1-.065 1.413 2.405 2.405 0 0 1-.453.704c-.5.546-1.198.752-1.497.579-.323-.188-.161-.956.418-1.568.623-.66 1.52-1.083 1.52-1.083l-.002-.002.079-.043z" fill="#ec407a" fill-rule="nonzero" stroke="#ec407a" stroke-width=".5199012000000001"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-scripts" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#546e7a" fill-rule="nonzero"/><path d="M18.466 20.241c.69 0 1.259-.568 1.259-1.258v-8.18H15.32a.632.632 0 0 0-.63.63v6.292h-1.887v-6.922c0-1.036.852-1.888 1.888-1.888h6.921c1.036 0 1.888.852 1.888 1.888v.63h-2.517v8.18a1.896 1.896 0 0 1-1.888 1.887h-6.292a1.896 1.896 0 0 1-1.888-1.888v-.629h6.293c0 .69.568 1.258 1.258 1.258z" fill-rule="nonzero" fill="#cfd8dc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-scripts-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#546e7a" fill-rule="nonzero"/><path d="M18.466 20.241c.69 0 1.259-.568 1.259-1.258v-8.18H15.32a.632.632 0 0 0-.63.63v6.292h-1.887v-6.922c0-1.036.852-1.888 1.888-1.888h6.921c1.036 0 1.888.852 1.888 1.888v.63h-2.517v8.18a1.896 1.896 0 0 1-1.888 1.887h-6.292a1.896 1.896 0 0 1-1.888-1.888v-.629h6.293c0 .69.568 1.258 1.258 1.258z" fill-rule="nonzero" fill="#cfd8dc"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-src" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#4caf50" fill-rule="nonzero"/><g fill="#c8e6c9" transform="translate(2.065 -.225) scale(.70678)"><path d="M19.146 30.989a.902.902 0 0 1-.207-.025 1.045 1.045 0 0 1-.726-1.213l2.709-14.431c.049-.279.209-.525.444-.683a.891.891 0 0 1 .7-.122c.519.152.837.684.727 1.213L20.077 30.16a1.032 1.032 0 0 1-.442.681.895.895 0 0 1-.489.148zM24.578 28.944h-.068a.932.932 0 0 1-.668-.377 1.104 1.104 0 0 1 .1-1.419l4.658-4.553-4.638-4.239a1.105 1.105 0 0 1-.141-1.416.938.938 0 0 1 .661-.4.9.9 0 0 1 .709.237l5.47 5c.386.372.448.974.144 1.416a1.05 1.05 0 0 1-.142.163l-5.447 5.324a.913.913 0 0 1-.638.264zM16.423 28.947a.917.917 0 0 1-.639-.267l-5.452-5.327a.874.874 0 0 1-.132-.153 1.097 1.097 0 0 1 .141-1.414l5.471-5a.882.882 0 0 1 .7-.238.939.939 0 0 1 .665.4 1.104 1.104 0 0 1-.14 1.417L12.4 22.6l4.659 4.551c.377.382.42.988.1 1.419a.928.928 0 0 1-.669.377z" fill-rule="nonzero"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-src-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#4caf50"/><g fill="#c8e6c9" fill-rule="evenodd" transform="translate(2.064 -.224) scale(.70678)"><path d="M19.146 30.989a.902.902 0 0 1-.207-.025 1.045 1.045 0 0 1-.726-1.213l2.709-14.431c.049-.279.209-.525.444-.683a.891.891 0 0 1 .7-.122c.519.152.837.684.727 1.213L20.077 30.16a1.032 1.032 0 0 1-.442.681.895.895 0 0 1-.489.148zM24.578 28.944h-.068a.932.932 0 0 1-.668-.377 1.104 1.104 0 0 1 .1-1.419l4.658-4.553-4.638-4.239a1.105 1.105 0 0 1-.141-1.416.938.938 0 0 1 .661-.4.9.9 0 0 1 .709.237l5.47 5c.386.372.448.974.144 1.416a1.05 1.05 0 0 1-.142.163l-5.447 5.324a.913.913 0 0 1-.638.264zM16.423 28.947a.917.917 0 0 1-.639-.267l-5.452-5.327a.874.874 0 0 1-.132-.153 1.097 1.097 0 0 1 .141-1.414l5.471-5a.882.882 0 0 1 .7-.238.939.939 0 0 1 .665.4 1.104 1.104 0 0 1-.14 1.417L12.4 22.6l4.659 4.551c.377.382.42.988.1 1.419a.928.928 0 0 1-.669.377z" fill-rule="nonzero"/></g></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-test" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#1de9b6" fill-rule="nonzero"/><path d="M14 8.097v1.39h.695v9.732A2.794 2.794 0 0 0 17.475 22a2.794 2.794 0 0 0 2.781-2.78V9.486h.695v-1.39H14m2.78 9.732c-.417 0-.695-.278-.695-.695 0-.417.278-.695.696-.695.417 0 .695.278.695.695 0 .417-.278.695-.695.695m1.39-2.78c-.417 0-.695-.278-.695-.696 0-.417.278-.695.695-.695.417 0 .695.278.695.695 0 .418-.278.696-.695.696m.695-3.476h-2.78V9.487h2.78v2.086z" fill="#00897b" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-test-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#1de9b6" fill-rule="nonzero"/><path d="M14 8.097v1.39h.695v9.732A2.794 2.794 0 0 0 17.475 22a2.794 2.794 0 0 0 2.781-2.78V9.486h.695v-1.39H14m2.78 9.732c-.417 0-.695-.278-.695-.695 0-.417.278-.695.696-.695.417 0 .695.278.695.695 0 .417-.278.695-.695.695m1.39-2.78c-.417 0-.695-.278-.695-.696 0-.417.278-.695.695-.695.417 0 .695.278.695.695 0 .418-.278.696-.695.696m.695-3.476h-2.78V9.487h2.78v2.086z" fill="#00897b" fill-rule="nonzero"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-tools" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#1e88e5" fill-rule="nonzero"/><path d="M21.043 15.266a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141-2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-3.569 0a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141 2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m3.925-6.424a6.424 6.424 0 0 0-6.423 6.424 6.424 6.424 0 0 0 6.423 6.424 1.07 1.07 0 0 0 1.071-1.07c0-.28-.107-.53-.278-.715a1.105 1.105 0 0 1-.271-.713 1.07 1.07 0 0 1 1.07-1.071h1.263a3.569 3.569 0 0 0 3.57-3.569c0-3.154-2.877-5.71-6.425-5.71z" fill="#bbdefb" stroke-width=".714"/></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-tools-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#1e88e5"/><path d="M21.043 15.266a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141-2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-3.569 0a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m-2.141 2.855a1.07 1.07 0 0 1-1.07-1.07 1.07 1.07 0 0 1 1.07-1.071 1.07 1.07 0 0 1 1.07 1.07 1.07 1.07 0 0 1-1.07 1.071m3.925-6.424a6.424 6.424 0 0 0-6.423 6.424 6.424 6.424 0 0 0 6.423 6.424 1.07 1.07 0 0 0 1.071-1.07c0-.28-.107-.53-.278-.715a1.105 1.105 0 0 1-.271-.713 1.07 1.07 0 0 1 1.07-1.071h1.263a3.569 3.569 0 0 0 3.57-3.569c0-3.154-2.877-5.71-6.425-5.71z" fill="#bbdefb" stroke-width=".714"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-views" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#ff8a65" fill-rule="nonzero"/><path d="M12.487 21.868L11.384 9.5H23.5l-1.104 12.366-4.961 1.375-4.948-1.373zm4.464-3.2l-3.926-2.36v-.855l3.926-2.361v1.323l-2.504 1.465 2.504 1.465v1.323zm.982-.001v-1.323l2.522-1.464-2.522-1.464v-1.323l3.926 2.35v.874l-3.926 2.35z" fill="#e44d26"/></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="folder-views-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#ff8a65" fill-rule="nonzero"/><path d="M12.487 21.868L11.384 9.5H23.5l-1.104 12.366-4.961 1.375-4.948-1.373zm4.464-3.2l-3.926-2.36v-.855l3.926-2.361v1.323l-2.504 1.465 2.504 1.465v1.323zm.982-.001v-1.323l2.522-1.464-2.522-1.464v-1.323l3.926 2.35v.874l-3.926 2.35z" fill="#e44d26"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vscode" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#42a5f5" fill-rule="nonzero"/><path d="M20.811 8.52l-5.988 5.506-3.346-2.522-1.383.805 3.298 3.03-3.298 3.032 1.383.807 3.346-2.522 5.988 5.503 2.921-1.419V9.94zm0 3.622v6.396l-4.245-3.198z" fill="#bbdefb" stroke-width=".974"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vscode-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#42a5f5" fill-rule="nonzero"/><path d="M20.81 8.52l-5.988 5.506-3.346-2.522-1.384.805 3.3 3.03-3.3 3.032 1.384.807 3.346-2.522 5.988 5.503 2.921-1.419V9.94zm0 3.621v6.397l-4.245-3.198z" fill="#bbdefb" stroke-width=".974"/></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vue" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#009688" fill-rule="nonzero"/><g transform="translate(8.459 6.362) scale(.69572)"><path d="M1.821 4.15l10.21 17.618L22.239 4.235v-.084h-7.692l-2.434 4.178-2.422-4.178z" fill="#41b883"/><path d="M5.937 4.15l6.152 10.616 6.18-10.617h-3.722l-2.434 4.179-2.422-4.179z" fill="#35495e"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-vue-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#009688"/><g transform="translate(8.458 6.362) scale(.69572)"><path d="M1.821 4.15l10.21 17.618L22.239 4.235v-.084h-7.692l-2.434 4.178-2.422-4.178z" fill="#41b883"/><path d="M5.937 4.15l6.152 10.616 6.18-10.617h-3.722l-2.434 4.179-2.422-4.179z" fill="#35495e"/></g></symbol><symbol clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-webpack" xmlns="http://www.w3.org/2000/svg"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.097.903 2 2 2h16c1.097 0 2-.903 2-2V8a2 2 0 0 0-2-2h-8l-2-2z" fill="#03a9f4" fill-rule="nonzero"/><g transform="translate(9.192 7.48) scale(.66328)"><path d="M19.376 15.988l-7.708 4.45-7.709-4.45v-8.9l7.709-4.451 7.708 4.45z" fill="#fff" fill-opacity=".785"/><path d="M12.286 1.98c-.21 0-.41.059-.57.179l-7.9 4.44c-.32.17-.53.5-.53.88v9c0 .38.21.711.53.881l7.9 4.44c.16.12.36.18.57.18s.41-.06.57-.18l7.9-4.44c.32-.17.53-.5.53-.88v-9c0-.38-.21-.712-.53-.882l-7.9-4.44a.945.945 0 0 0-.57-.179zm0 2.15l7 3.94v2.103h-.016v5.177h.016v.54l-7 3.939-7-3.94V8.07zm0 2.08l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#8ed6fb"/><path d="M12.286 6.21l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#1c78c0"/></g></symbol><symbol clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" viewBox="0 0 24 24" id="folder-webpack-open" xmlns="http://www.w3.org/2000/svg"><path d="M19 20H4a2 2 0 0 1-2-2V6c0-1.11.89-2 2-2h6l2 2h7c1.097 0 2 .903 2 2H4v10l2.14-8h17.07l-2.28 8.5c-.23.87-1.01 1.5-1.93 1.5z" fill="#03a9f4"/><g transform="translate(9.193 7.48) scale(.66328)"><path d="M19.376 15.988l-7.708 4.45-7.709-4.45v-8.9l7.709-4.451 7.708 4.45z" fill="#fff" fill-opacity=".785"/><path d="M12.286 1.98c-.21 0-.41.059-.57.179l-7.9 4.44c-.32.17-.53.5-.53.88v9c0 .38.21.711.53.881l7.9 4.44c.16.12.36.18.57.18s.41-.06.57-.18l7.9-4.44c.32-.17.53-.5.53-.88v-9c0-.38-.21-.712-.53-.882l-7.9-4.44a.945.945 0 0 0-.57-.179zm0 2.15l7 3.94v2.103h-.016v5.177h.016v.54l-7 3.939-7-3.94V8.07zm0 2.08l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#8ed6fb"/><path d="M12.286 6.21l-4.9 2.83 4.9 2.83 4.9-2.83zm-5 5.08v3.58l4 2.309v-3.58zm10 0l-4 2.308v3.58l4-2.308z" fill="#1c78c0"/></g></symbol><symbol viewBox="0 0 24 24" id="font" xmlns="http://www.w3.org/2000/svg"><path d="M9.62 12L12 5.67 14.37 12M11 3L5.5 17h2.25l1.12-3h6.25l1.13 3h2.25L13 3h-2z" fill="#f44336"/></symbol><symbol viewBox="0 0 500 500" id="fsharp" xmlns="http://www.w3.org/2000/svg"><path d="M235.906 36.66L21.963 250.601l213.943 213.943v-84.36L106.209 250.487l129.697-129.696z" fill="#378bba" stroke-width="14.706"/><path d="M235.906 156.614l-93.622 93.62 93.622 93.622z" fill="#378bba" stroke-width="15.006"/><path d="M263.417 36.64L477.36 250.583 263.417 464.526v-84.36l129.696-129.697-129.696-129.696z" fill="#30b9db" stroke-width="14.706"/></symbol><symbol viewBox="0 0 152.99 160.01" id="fusebox" xmlns="http://www.w3.org/2000/svg"><defs id="fkdefs4"><style id="fkstyle2">.fkcls-1{fill:#fff}.fkcls-2{fill:#515151}.fkcls-3{fill:#1d79bf}.fkcls-4{fill:#383838}</style></defs><title id="fktitle6">Asset 3</title><g id="fkLayer_2" data-name="Layer 2" transform="matrix(.87285 0 0 .87285 10.17 10.175)"><g id="fkFuse_Box" data-name="Fuse Box"><g id="fkLOGO"><path class="fkcls-1" id="fkpolygon8" fill="#fff" d="M76.56 2.19l74.22 24.93-7.7 87.77-65.41 42.66-69.79-43.93-5.7-86.13z"/><path class="fkcls-2" d="M77.69 160L5.87 114.81 0 26 76.55 0 153 25.67l-7.94 90.4zM9.88 112.43l67.77 42.66 63.45-41.39 7.47-85.13-72-24.18L4.36 28.95z" id="fkpath10" fill="#515151"/><path class="fkcls-3" id="fkpolygon12" fill="#1d79bf" d="M76.4 148.8V61.68l66.93-29.82-5.99 78.77z"/><path id="fkF" class="fkcls-4" fill="#383838" d="M76.4 148.8l-60.35-37.39L9.63 31.8 76.4 61.68z"/><path class="fkcls-1" d="M25.58 52.73l.54 15.93 37.35 18.18.12 14.69-37-18.21 1.64 37.1-14.56-9-5.05-80.55 67.79 30.82v15.46z" id="fkpath15" fill="#fff"/><path class="fkcls-1" d="M135.91 90.77c-.08 13.12-6.33 26.59-16.77 33.12l-42.8 27.93V61.71l42.27-18.84c5.16-2.41 9.51-1.43 12.4 3.11 1.9 3 2.89 7.23 2.86 12.21A35.69 35.69 0 0 1 129.34 76c4.29 2 6.66 6.55 6.57 14.77zM123 63.76c0-4.64-2-6.93-4.92-5.45l-29 14.48L89 90l29.44-15.59c2.5-1.32 4.56-5.91 4.56-10.65zM125.15 96c0-5.71-2.42-8.24-6.55-5.93L89 106.64v19.58l29.34-17.46c4.43-2.64 6.79-7.27 6.81-12.76z" id="fkpath17" fill="#fff"/><path id="fkTOP" class="fkcls-4" fill="#383838" d="M76.4 8.82L9.71 31.77l109.77 2.38-84.02 9.21L76.4 61.68l20.76-9.25-27.73-1.37 49.78-8.46 24.12-10.74z"/></g></g></g></symbol><symbol viewBox="0 0 24 24" id="git" xmlns="http://www.w3.org/2000/svg"><path d="M2.6 10.59L8.38 4.8l1.69 1.7c-.24.85.15 1.78.93 2.23v5.54c-.6.34-1 .99-1 1.73a2 2 0 0 0 2 2 2 2 0 0 0 2-2c0-.74-.4-1.39-1-1.73V9.41l2.07 2.09c-.07.15-.07.32-.07.5a2 2 0 0 0 2 2 2 2 0 0 0 2-2 2 2 0 0 0-2-2c-.18 0-.35 0-.5.07L13.93 7.5a1.98 1.98 0 0 0-1.15-2.34c-.43-.16-.88-.2-1.28-.09L9.8 3.38l.79-.78c.78-.79 2.04-.79 2.82 0l7.99 7.99c.79.78.79 2.04 0 2.82l-7.99 7.99c-.78.79-2.04.79-2.82 0L2.6 13.41c-.79-.78-.79-2.04 0-2.82z" fill="#e64a19"/></symbol><symbol viewBox="0 0 494 455" id="gitlab" xmlns="http://www.w3.org/2000/svg"><title>logo</title><defs><path id="fma" d="M0 1173.3h2000V0H0v1173.3z"/></defs><g transform="matrix(.88256 0 0 -.88256 -286.767 742.766)" fill="none" fill-rule="evenodd"><mask id="fmb" fill="#fff"><use width="100%" height="100%" xlink:href="#fma"/></mask><g><g transform="translate(358.67 358.67)"><path d="M492.532 195.445l-27.559 84.815-54.617 168.1c-2.81 8.648-15.045 8.648-17.856 0l-54.619-168.1h-181.37l-54.62 168.1c-2.81 8.648-15.045 8.648-17.856 0l-54.617-168.1-27.557-84.815a18.775 18.775 0 0 1 6.82-20.992l238.51-173.29 238.51 173.29a18.777 18.777 0 0 1 6.82 20.992" fill="#fc6d26"/><path d="M247.2 1.16l90.684 279.1h-181.37z" fill="#e24329"/><path d="M247.201 1.16l-90.684 279.09H29.427z" fill="#fc6d26"/><path d="M29.422 280.256L1.862 195.44a18.774 18.774 0 0 1 6.822-20.991L247.194 1.16z" fill="#fca326"/><path d="M29.422 280.26h127.09l-54.619 168.1c-2.81 8.65-15.047 8.65-17.856 0z" fill="#e24329"/><path d="M247.2 1.16l90.684 279.09h127.09z" fill="#fc6d26"/><path d="M464.98 280.256l27.559-84.815a18.774 18.774 0 0 0-6.821-20.991L247.208 1.16z" fill="#fca326"/><path d="M464.97 280.26H337.88l54.619 168.1c2.81 8.65 15.047 8.65 17.856 0z" fill="#e24329"/></g></g></g></symbol><symbol viewBox="0 0 24 24" id="go" xmlns="http://www.w3.org/2000/svg"><path d="M10.575 1.695c-2.634 0-4.756 2.453-4.756 5.502v4.6l-.027-.003v4.71c0 3.05 2.123 5.502 4.757 5.502h2.286c2.634 0 4.757-2.453 4.757-5.502v-4.6a5.1 5.1 0 0 0 .026.003v-4.71c0-3.049-2.122-5.502-4.756-5.502h-2.287z" fill="#73cddc"/><rect width="2.289" height="3.335" x="-1.178" y="6.092" ry="1.125" transform="matrix(.4849 -.87457 .85979 .51065 0 0)" fill="#73cddc"/><rect width="2.297" height="3.39" x="10.261" y="-15.076" ry="1.143" transform="matrix(.44646 .8948 -.89204 .45195 0 0)" fill="#73cddc"/><circle cx="9.267" cy="5.13" r="2.054" fill="#fff" stroke="#5e5d5b" stroke-width=".1"/><circle cx="14.214" cy="5.116" r="2.054" fill="#fff" stroke="#5e5d5b" stroke-width=".1"/><ellipse cx="8.039" cy="5.051" rx=".792" ry=".901" fill="#030d18"/><path d="M11.792 9.556l.763.138a.403.689 0 0 1 .008.138.403.689 0 0 1-.402.69.403.689 0 0 1-.404-.69.403.689 0 0 1 .035-.276z" fill="#fff" stroke="#fff" stroke-width=".155"/><ellipse cx="8.51" cy="5.365" rx=".138" ry=".166" fill="#fff"/><ellipse cx="12.945" cy="5.189" rx=".792" ry=".901" fill="#030d18"/><ellipse cx="13.414" cy="5.446" rx=".138" ry=".166" fill="#fff"/><ellipse cx="-12.982" cy="-3.409" rx=".708" ry="1.026" transform="rotate(-129.403)" fill="#f6d2a1" stroke-width=".4"/><path d="M11.772 9.553l-.757.135a.4.672 0 0 0-.008.135.4.672 0 0 0 .4.672.4.672 0 0 0 .4-.672.4.672 0 0 0-.035-.27z" fill="#fff" stroke="#fff" stroke-width=".153"/><ellipse cx="1.841" cy="-21.563" rx=".707" ry="1.026" transform="scale(1 -1) rotate(50.597)" fill="#f6d2a1" stroke-width=".4"/><ellipse cx="-17.281" cy="-21.784" rx=".864" ry="1.27" transform="matrix(.3054 -.95222 -.97065 -.2405 0 0)" fill="#f6d2a1" stroke-width=".4"/><ellipse cx="22.885" cy="2.587" rx=".864" ry="1.27" transform="matrix(.22652 .974 .95652 -.29167 0 0)" fill="#f6d2a1" stroke-width=".4"/><path d="M10.708 8.392a.594.594 0 0 0-.594.597v.115c0 .331.264.598.594.598h.386a.973.772 0 0 1 .697-.235.973.772 0 0 1 .698.235h.334c.33 0 .594-.267.594-.598V8.99a.595.595 0 0 0-.594-.597h-2.115z" fill="#f6d2a1" stroke="#657075" stroke-width=".1"/><ellipse cx="11.734" cy="8.203" rx="1.208" ry=".68" fill="#030d18" stroke="#fff" stroke-width=".162"/></symbol><symbol viewBox="0 0 24 24" id="gradle" xmlns="http://www.w3.org/2000/svg"><path d="M21.718 5.503c-.731-1.315-2.04-1.708-2.963-1.727-1.133-.023-2.065.605-1.888 1.017.037.088.25.55.38.741.19.275.527.064.646 0 .353-.187.73-.248 1.16-.198.409.048.954.3 1.319 1.001.859 1.652-1.794 5.05-5.114 2.697-3.32-2.353-6.548-1.574-8.01-1.1-1.462.475-2.135.952-1.556 2.055.785 1.498.524 1.038 1.285 2.28 1.21 1.97 3.856-.908 3.856-.908-1.972 2.906-3.662 2.204-4.31 1.188a15.864 15.864 0 0 1-1.038-1.97c-4.993 1.76-3.642 9.534-3.642 9.534h2.48c.632-2.862 2.892-2.757 3.28 0h1.892c1.673-5.59 5.914 0 5.914 0h2.466c-.69-3.812 1.388-5.01 2.697-7.246 1.31-2.235 2.551-4.969 1.146-7.364zm-6.362 7.362c-1.304-.426-.837-1.723-.837-1.723s1.139.368 2.68.87c-.09.403-.856 1.175-1.843.853z" fill="#0097a7" stroke-width=".47"/></symbol><symbol preserveAspectRatio="xMidYMid" viewBox="0 0 300 300" id="graphcool" xmlns="http://www.w3.org/2000/svg"><path d="M246.886 107.727c-12.237-6.892-27.616 2.1-30.081 3.646l-52.834 29.965c-7.8-6.196-18.914-5.933-26.412.625-7.499 6.558-9.24 17.537-4.14 26.094 5.102 8.556 15.588 12.246 24.923 8.768 9.335-3.478 14.852-13.129 13.111-22.937l52.688-29.9.321-.196c3.464-2.188 11.5-5.462 15.256-3.34 2.706 1.524 4.252 6.629 4.376 14.148h-.066v66.092a17.313 17.313 0 0 1-8.635 14.95l-75.739 43.755a17.312 17.312 0 0 1-17.261 0l-75.74-43.756a17.312 17.312 0 0 1-8.634-14.95V113.22c.01-6.165 3.3-11.86 8.634-14.95l68.549-39.562c6.522 7.482 17.451 9.25 26 4.206s12.283-15.468 8.886-24.794c-3.397-9.327-12.962-14.904-22.751-13.27-9.79 1.636-17.022 10.02-17.204 19.944L59.397 85.632a31.932 31.932 0 0 0-15.978 27.588v87.454a31.933 31.933 0 0 0 15.927 27.602l75.74 43.755a31.934 31.934 0 0 0 31.846 0l75.74-43.755a31.933 31.933 0 0 0 15.927-27.58V137.12h.05c.373-14.913-3.616-24.794-11.762-29.389z" fill="#27ae60" stroke="#27ae60" stroke-width="7.883622079999999"/></symbol><symbol viewBox="0 0 400 400" id="graphql" xmlns="http://www.w3.org/2000/svg"><path d="M67.008 293.022l-13.143-7.588L200.282 31.839l13.143 7.588z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M50.855 265.174H343.69v15.177H50.855z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M203.122 358.269L56.649 273.7l7.589-13.143 146.472 84.568zm127.24-220.407L183.889 53.293l7.589-13.143 146.472 84.568z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M64.278 137.803l-7.588-13.142 146.472-84.568 7.588 13.143z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M327.661 293.025L181.244 39.43l13.143-7.589 146.417 253.596zM62.466 114.597h15.176v169.136H62.466zm254.528 0h15.176v169.136h-15.176z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M200.538 351.845l-6.628-11.481L321.3 266.812l6.629 11.48z" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/><path d="M352.284 288.67c-8.777 15.268-28.342 20.48-43.61 11.703-15.268-8.777-20.48-28.342-11.703-43.61 8.777-15.268 28.342-20.48 43.61-11.703 15.36 8.869 20.57 28.342 11.703 43.61M97.574 141.567c-8.778 15.268-28.343 20.48-43.61 11.703-15.269-8.777-20.48-28.342-11.703-43.61 8.777-15.268 28.342-20.48 43.61-11.703 15.268 8.869 20.479 28.342 11.702 43.61M42.353 288.67c-8.777-15.268-3.566-34.741 11.702-43.61 15.268-8.776 34.741-3.565 43.61 11.703 8.776 15.268 3.565 34.741-11.703 43.61-15.36 8.776-34.833 3.565-43.61-11.703m254.71-147.103c-8.776-15.268-3.565-34.741 11.703-43.61 15.268-8.776 34.742-3.565 43.61 11.703 8.777 15.268 3.566 34.741-11.702 43.61-15.268 8.776-34.833 3.565-43.61-11.703m-99.745 236.608c-17.645 0-31.907-14.262-31.907-31.907s14.262-31.907 31.907-31.907 31.907 14.262 31.907 31.907c0 17.554-14.262 31.907-31.907 31.907m0-294.206c-17.645 0-31.907-14.262-31.907-31.907s14.262-31.907 31.907-31.907 31.907 14.262 31.907 31.907-14.262 31.907-31.907 31.907" fill="#ec407a" stroke-width="6.803" stroke="#ec407a"/></symbol><symbol viewBox="0 0 24 24" id="groovy" xmlns="http://www.w3.org/2000/svg"><path d="M12 1.982a10.119 10.119 0 0 0-10.12 10.12A10.119 10.119 0 0 0 12 22.22 10.119 10.119 0 0 0 22.12 12.1 10.119 10.119 0 0 0 12 1.983zm1.254 2.422c.91 0 1.647.261 2.213.78.571.518.857 1.188.857 2.013 0 .889-.319 1.673-.959 2.35-.64.677-1.376 1.015-2.207 1.015-.486 0-.89-.119-1.213-.357-.317-.238-.476-.532-.476-.88 0-.212.06-.4.181-.563.127-.164.274-.246.438-.246.159 0 .238.092.238.277 0 .164.06.29.182.38.121.09.261.136.42.136.423 0 .828-.29 1.215-.866.391-.582.587-1.202.587-1.863 0-.465-.151-.844-.453-1.135-.301-.296-.69-.445-1.166-.445-.714 0-1.406.318-2.078.953-.666.635-1.211 1.47-1.635 2.506-.417 1.031-.627 2.014-.627 2.945 0 .857.185 1.54.555 2.047.37.503.863.754 1.477.754 1.037 0 2.027-.734 2.974-2.2l1.493-.212c.185-.026.277.018.277.135 0 .053-.072.28-.215.681-.143.402-.337 1.074-.586 2.016.82-.476 1.455-1.003 1.904-1.58v.914c-.36.418-1.046.888-2.062 1.412-.212 1.407-.682 2.493-1.406 3.26-.725.772-1.54 1.16-2.444 1.16-.433 0-.775-.102-1.023-.303-.243-.2-.365-.477-.365-.832 0-.984.955-1.94 2.865-2.865.2-.714.395-1.356.586-1.928-.333.482-.817.907-1.451 1.278-.635.37-1.225.554-1.77.554-.889 0-1.628-.383-2.22-1.15-.588-.772-.881-1.748-.881-2.928 0-1.243.333-2.42 1-3.531a7.747 7.747 0 0 1 2.625-2.674c1.084-.672 2.134-1.008 3.15-1.008zM12.03 16.592c-1.375.687-2.062 1.365-2.062 2.031 0 .354.169.533.508.533.666 0 1.184-.856 1.554-2.564z" fill="#26c6da"/></symbol><symbol viewBox="0 0 24 24" id="gulp" xmlns="http://www.w3.org/2000/svg"><path d="M8.37 15.94a596.238 596.238 0 0 1-.482-4.982c.002-.042-.225-.077-.505-.077h-.508V8.95h3.966V5.198l1.871-1.124c1.14-.685 1.978-1.125 2.144-1.125.4 0 .866.506.866.939 0 .19-.057.422-.127.517-.07.095-.722.53-1.45.966l-1.321.792-.029 1.393-.028 1.393h3.972v1.932h-.98l-.495 4.983-.495 4.983H8.854l-.485-4.906z" fill="#e53935"/></symbol><symbol viewBox="0 0 24 24" id="h" xmlns="http://www.w3.org/2000/svg"><path d="M16.745 19.818h-3.007v-5.882q0-2.381-1.736-2.381-.869 0-1.438.663-.56.662-.56 1.718v5.882H6.988V4.533h3.016v6.508h.037q1.186-1.802 3.193-1.802 3.511 0 3.511 4.239z" stroke-width=".478" fill="#0277bd"/></symbol><symbol viewBox="0 0 253.6 253.6" id="hack" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-29.243 -29.515) scale(1.2301)"><path fill="#607d8b" d="M69.496 159.551v52.576l51.77-52.576zM123.507 41.523l-54.01 52.755v55.084l54.01-54.009z"/><path fill="#eceff1" d="M130.023 95.663v51.501l52.128-51.5z"/><path fill="#607d8b" d="M185.465 101.867l-55.442 55.174v55.083l55.442-55.262z"/><path fill="#ffa000" d="M73.068 154.283l50.427.09v-50.248z"/></g></symbol><symbol viewBox="0 0 300 300.00001" id="haml" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 165.6)"><path d="M78.42-132.307c-12.047-.302-26.924 5.998-26.924 5.998l49.195 99.791L74.605 85.005c23.81 20.134 50.07 10.504 50.07 10.504L136.76 9.212c1.526 1.446 3.146 2.77 4.777 3.995 5.244 3.714 10.925 6.553 16.606 8.738 5.68 2.185 11.583 3.933 17.482 5.244 3.933.874 7.645 1.53 11.578 1.967-1.748 3.933-2.84 8.083-2.621 12.672 0 .437.22.873.656 1.092h.217c4.152 2.185 8.521 3.934 13.328 5.027 4.589.874 9.615 1.312 14.422.656 5.026-.655 10.051-2.623 13.984-5.9 3.933-3.278 6.774-7.648 8.522-12.237l.219-.218v-.217l.656-5.899v-.22c2.185-1.311 4.37-2.621 6.555-4.37 2.622-2.184 5.025-4.589 6.773-7.648 1.748-3.059 2.84-6.774 2.621-10.488-.218-3.496-1.53-6.99-3.06-10.049-1.53-3.059-3.495-5.901-5.68-8.523-4.37-5.026-9.614-9.176-15.295-12.454-5.462-3.496-11.581-6.338-17.7-8.304l-2.404-.656-1.962-.655c-1.311-.437-2.406-1.092-3.498-1.53-2.185-1.31-3.717-2.622-4.809-4.37-2.185-3.278-2.403-8.301-1.31-13.545.218-1.311.656-2.623 1.093-3.934a96.064 96.064 0 0 0 1.31-4.152c.314-1.412.51-2.829.598-4.402l29.203-25.553c-2.275-8.404-27.488-17.158-27.488-17.158l-74.931 63.726-43.243-81.584c-1.553-.35-3.218-.527-4.94-.57zm107.682 73.14c-.449 2.336-.647 4.795-.647 7.258.219 3.715 1.311 7.87 3.715 11.366 2.403 3.496 5.68 6.117 8.957 7.646a29.663 29.663 0 0 0 5.027 1.967l2.623.654 2.184.438c5.68 1.53 11.142 3.714 16.168 6.554 5.025 2.84 9.833 6.337 13.766 10.27s6.992 8.959 7.43 13.984c.218 3.496-.22 6.118-1.313 8.303-1.093 2.404-2.84 4.588-4.807 6.555-.874.874-1.966 1.747-2.84 2.402a27.11 27.11 0 0 0-.654-5.898c-.219-1.093-.438-1.966-.875-3.059-.437-.874-.872-1.966-1.965-2.621-.218 0-.44-.001-.44.217-1.31 3.277-3.494 6.12-5.898 8.086-2.403 1.966-5.462 2.84-8.521 3.058-3.06.219-6.338-.436-9.616-1.31-3.277-.874-6.552-1.968-9.83-3.06l-.439-.22c-.656-.218-1.526.002-1.963.44-1.748 2.185-3.06 4.149-4.59 6.334a58.435 58.435 0 0 0-2.84 5.027c-3.933-1.53-7.649-2.841-11.582-4.37-5.462-2.186-10.925-4.37-15.95-6.991-5.245-2.404-10.268-5.246-14.638-8.524-3.15-2.363-6.062-4.845-8.185-7.681l2.404-17.172z" fill="#f4511e" stroke-width="0" stroke-linejoin="round"/></g></symbol><symbol viewBox="0 0 24 24" id="handlebars" xmlns="http://www.w3.org/2000/svg"><path d="M8.55 10.32c-2.753 0-4.202 3.48-5.793 3.48-.98 0-1.126-.677-1.126-.915 0-.332.236-.706.564-.706.59 0 .414.77.414.77s.798-.555.272-1.298c-.42-.595-1.31-.623-1.92-.17-.617.458-1.057 1.146-.853 2.287.1.551.468 1.35 1.233 1.805.764.455 1.925.566 2.335.566 2.194 0 4.342-1.633 6.639-2.322a5.513 5.513 0 0 1 1.497-.222 6.19 6.19 0 0 1 1.92.226c2.296.689 4.444 2.323 6.638 2.323.41 0 1.57-.11 2.335-.566.765-.455 1.132-1.256 1.231-1.807.204-1.14-.235-1.829-.853-2.287-.61-.453-1.497-.423-1.918.172-.526.743.27 1.297.27 1.297s-.176-.77.414-.77c.329 0 .565.373.565.705 0 .238-.147.914-1.126.914-1.592 0-3.04-3.478-5.794-3.478-2.565 0-3.076 1.177-3.462 1.718-.004.005-.005.011-.008.016-.005-.006-.007-.013-.012-.02-.386-.54-.896-1.717-3.461-1.717z" fill="#ff7043" fill-rule="evenodd" stroke-width=".3"/></symbol><symbol viewBox="0 0 300.00001 300" id="haskell" xmlns="http://www.w3.org/2000/svg"><g stroke-width="2.422"><path d="M23.928 240.5l59.94-89.852-59.94-89.855h44.955l59.94 89.855-59.94 89.852z" fill="#ef5350"/><path d="M83.869 240.5l59.94-89.852-59.94-89.855h44.955l119.88 179.71h-44.95l-37.46-56.156-37.468 56.156z" fill="#ffa726"/><path d="M228.72 188.08l-19.98-29.953h69.93v29.956h-49.95zm-29.97-44.924l-19.98-29.953h99.901v29.953z" fill="#ffee58"/></g></symbol><symbol viewBox="0 0 210 210" id="haxe" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -87)"><path fill="#f68712" stroke-width=".221" d="M42.78 191.545l63.431-63.43 63.431 63.43-63.431 63.431z"/><path d="M42.8 191.592L31.193 148.28 19.59 104.97 62.9 116.575l43.311 11.605-31.706 31.706z" fill="#fab20b" stroke-width=".266"/><path d="M105.956 128.111l-43.19-11.544-43.177-11.597 22.927.185 23.228.294 20.264 11.36z" fill="#fbc707" stroke-width=".265"/><path d="M19.59 104.97l11.596 43.176 11.545 43.19-11.303-19.948-11.36-20.263-.294-23.228z" fill="#fff200" stroke-width=".265"/><path d="M106.23 128.133l43.312-11.605 43.311-11.605-11.605 43.31-11.605 43.312-31.706-31.706z" fill="#f47216" stroke-width=".266"/><path d="M169.711 191.289l11.545-43.19 11.597-43.176-.185 22.927-.294 23.228-11.36 20.263z" fill="#f1471d" stroke-width=".265"/><path d="M192.853 104.923l-43.176 11.597-43.19 11.544 19.947-11.303 20.264-11.36 23.228-.293z" fill="#fbc707" stroke-width=".265"/><path d="M169.643 191.545l11.605 43.31 11.605 43.312-43.311-11.605-43.311-11.606 31.706-31.705z" fill="#f25c19" stroke-width=".266"/><path d="M106.487 255.025l43.19 11.544 43.176 11.598-22.927-.185-23.228-.294-20.264-11.36z" fill="#f68712" stroke-width=".265"/><path d="M192.853 278.167l-11.597-43.176-11.545-43.19 11.303 19.947 11.36 20.264.294 23.228z" fill="#f1471d" stroke-width=".265"/><path d="M106.211 254.976l-43.31 11.605-43.312 11.605 11.605-43.31L42.8 191.563l31.706 31.706z" fill="#f89c0e" stroke-width=".266"/><path d="M42.731 191.82l-11.545 43.19-11.597 43.176.185-22.927.294-23.228 11.36-20.263z" fill="#fff200" stroke-width=".265"/><path d="M19.59 278.186l43.175-11.597 43.19-11.544-19.947 11.303-20.264 11.36-23.228.293z" fill="#f25c19" stroke-width=".265"/></g></symbol><symbol viewBox="0 0 144 152" id="heroku" xmlns="http://www.w3.org/2000/svg"><path d="M118.68 13.279H26.865c-6.337 0-11.476 5.139-11.476 11.476V129.32c0 6.338 5.139 11.477 11.476 11.477h91.813c6.338 0 11.477-5.14 11.477-11.477V24.755c0-6.337-5.139-11.476-11.477-11.476zM44.08 121.669V96.165l14.346 12.752zm44.632 0v-38.08c-.063-2.976-1.496-6.551-7.97-6.551-12.966 0-27.51 6.52-27.654 6.586l-9.008 4.08V32.407h12.752v36.201c6.366-2.072 15.266-4.321 23.91-4.321 7.882 0 12.6 3.099 15.17 5.698 5.484 5.547 5.56 12.613 5.551 13.43v38.255zm3.188-68.54H79.149c5.011-6.576 8.158-13.496 9.564-20.723h12.751c-.86 7.243-3.796 14.187-9.563 20.722z" fill="#6963b9"/></symbol><symbol viewBox="0 0 24 24" id="hpp" xmlns="http://www.w3.org/2000/svg"><path d="M9.757 19.818H6.751v-5.882q0-2.381-1.737-2.381-.868 0-1.438.663-.56.662-.56 1.718v5.882H0V4.533h3.016v6.508h.037Q4.24 9.239 6.247 9.239q3.51 0 3.51 4.239z" stroke-width=".478" fill="#0277bd"/><path d="M13.073 11.448v2h-2v2h2v2h2v-2h2v-2h-2v-2zm7 0v2h-2v2h2v2h2v-2h2v-2h-2v-2z" fill="#0277bd"/></symbol><symbol viewBox="0 0 24 24" id="html" xmlns="http://www.w3.org/2000/svg"><path d="M12 17.56l4.07-1.13.55-6.1H9.38L9.2 8.3h7.6l.2-1.99H7l.56 6.01h6.89l-.23 2.58-2.22.6-2.22-.6-.14-1.66h-2l.29 3.19L12 17.56M4.07 3h15.86L18.5 19.2 12 21l-6.5-1.8L4.07 3z" fill="#e44d26"/></symbol><symbol viewBox="0 0 24 24" id="http" xmlns="http://www.w3.org/2000/svg"><path d="M16.046 13.784c.074-.613.13-1.225.13-1.856s-.056-1.244-.13-1.856h3.137c.148.594.241 1.215.241 1.856a7.65 7.65 0 0 1-.241 1.856m-4.78 5.16c.557-1.03.984-2.144 1.281-3.304h2.738a7.452 7.452 0 0 1-4.019 3.304m-.232-5.16H9.828a12.314 12.314 0 0 1-.149-1.856c0-.631.056-1.253.149-1.856h4.343c.084.603.149 1.225.149 1.856 0 .63-.065 1.243-.149 1.856M12 19.315c-.77-1.113-1.393-2.348-1.773-3.675h3.545c-.38 1.327-1.002 2.562-1.773 3.675m-3.712-11.1h-2.71a7.353 7.353 0 0 1 4.01-3.304c-.557 1.03-.975 2.144-1.3 3.304m-2.71 7.425h2.71c.325 1.16.743 2.274 1.3 3.304a7.433 7.433 0 0 1-4.01-3.304m-.761-1.856a7.65 7.65 0 0 1-.241-1.856c0-.64.093-1.262.241-1.856h3.137c-.074.612-.13 1.225-.13 1.856 0 .63.056 1.243.13 1.856m4.046-9.253c.77 1.114 1.393 2.357 1.773 3.684h-3.545c.38-1.327 1.002-2.57 1.773-3.684m6.422 3.684h-2.738a14.523 14.523 0 0 0-1.28-3.304 7.412 7.412 0 0 1 4.018 3.304m-6.423-5.568c-5.132 0-9.28 4.176-9.28 9.28a9.28 9.28 0 0 0 9.28 9.282 9.28 9.28 0 0 0 9.281-9.281A9.28 9.28 0 0 0 12 2.647z" fill="#e53935" stroke-width=".928"/></symbol><symbol viewBox="0 0 24 24" id="image" xmlns="http://www.w3.org/2000/svg"><path d="M13.009 9.202h5.368l-5.368-5.368v5.368M6.177 2.37h7.808l5.856 5.856v11.711a1.952 1.952 0 0 1-1.952 1.952H6.178a1.951 1.951 0 0 1-1.952-1.952V4.322c0-1.083.868-1.952 1.952-1.952m0 17.567h11.71V12.13l-3.903 3.903-1.952-1.951-5.856 5.855M8.13 9.202a1.952 1.952 0 0 0-1.952 1.952 1.952 1.952 0 0 0 1.952 1.952 1.952 1.952 0 0 0 1.952-1.952A1.952 1.952 0 0 0 8.13 9.202z" fill="#26a69a" stroke-width=".976"/></symbol><symbol viewBox="0 0 512 512" id="ionic" xmlns="http://www.w3.org/2000/svg"><g fill="#4f8ff7"><path d="M423.592 132.804A31.855 31.855 0 0 0 429 115c0-17.675-14.33-32-32-32a31.853 31.853 0 0 0-17.805 5.409C344.709 63.015 302.11 48 256 48 141.125 48 48 141.125 48 256c0 114.877 93.125 208 208 208 114.873 0 208-93.123 208-208 0-46.111-15.016-88.71-40.408-123.196zM391.83 391.832c-17.646 17.646-38.191 31.499-61.064 41.174-23.672 10.012-48.826 15.089-74.766 15.089-25.94 0-51.095-5.077-74.767-15.089-22.873-9.675-43.417-23.527-61.064-41.174s-31.5-38.191-41.174-61.064C68.982 307.096 63.905 281.94 63.905 256c0-25.94 5.077-51.095 15.089-74.767 9.674-22.873 23.527-43.417 41.174-61.064s38.191-31.5 61.064-41.174c23.673-10.013 48.828-15.09 74.768-15.09 25.939 0 51.094 5.077 74.766 15.089a191.221 191.221 0 0 1 37.802 21.327A31.853 31.853 0 0 0 365 115c0 17.675 14.327 32 32 32 5.293 0 10.28-1.293 14.678-3.568a191.085 191.085 0 0 1 21.327 37.801c10.013 23.672 15.09 48.827 15.09 74.767 0 25.939-5.077 51.096-15.09 74.768-9.675 22.873-23.527 43.418-41.175 61.064z"/><circle cx="256.003" cy="256" r="96"/></g></symbol><symbol viewBox="0 0 24 24" id="java" xmlns="http://www.w3.org/2000/svg"><path d="M2 21h18v-2H2M20 8h-2V5h2m0-2H4v10a4 4 0 0 0 4 4h6a4 4 0 0 0 4-4v-3h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="javascript" xmlns="http://www.w3.org/2000/svg"><path d="M3 3h18v18H3V3m4.73 15.04c.4.85 1.19 1.55 2.54 1.55 1.5 0 2.53-.8 2.53-2.55v-5.78h-1.7V17c0 .86-.35 1.08-.9 1.08-.58 0-.82-.4-1.09-.87l-1.38.83m5.98-.18c.5.98 1.51 1.73 3.09 1.73 1.6 0 2.8-.83 2.8-2.36 0-1.41-.81-2.04-2.25-2.66l-.42-.18c-.73-.31-1.04-.52-1.04-1.02 0-.41.31-.73.81-.73.48 0 .8.21 1.09.73l1.31-.87c-.55-.96-1.33-1.33-2.4-1.33-1.51 0-2.48.96-2.48 2.23 0 1.38.81 2.03 2.03 2.55l.42.18c.78.34 1.24.55 1.24 1.13 0 .48-.45.83-1.15.83-.83 0-1.31-.43-1.67-1.03l-1.38.8z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="javascript-map" xmlns="http://www.w3.org/2000/svg"><path d="M18 8v2h2v10H10v-2H8v4h14V8h-4z" fill="#ffca28"/><path d="M2.444 2.506h14.135v14.136H2.444V2.506m3.714 11.811c.315.668.935 1.218 1.995 1.218 1.178 0 1.987-.629 1.987-2.003V8.993H8.805v4.508c0 .675-.275.848-.707.848-.455 0-.644-.314-.856-.683l-1.084.651m4.697-.14c.392.769 1.185 1.358 2.426 1.358 1.257 0 2.199-.652 2.199-1.854 0-1.107-.636-1.602-1.767-2.089l-.33-.141c-.573-.243-.816-.408-.816-.801 0-.322.243-.573.636-.573.377 0 .628.165.856.573l1.028-.683c-.432-.754-1.044-1.045-1.884-1.045-1.186 0-1.948.754-1.948 1.752 0 1.083.636 1.594 1.594 2.002l.33.141c.613.267.974.432.974.888 0 .377-.354.652-.903.652-.652 0-1.029-.338-1.312-.81l-1.083.63z" fill="#ffca28"/></symbol><symbol viewBox="0 0 180 180" id="jenkins" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="gia"><path transform="scale(1 -1)" fill="#37474f" d="M.899-144.42h144.42V0H.899z"/></clipPath></defs><g transform="matrix(1.0691 0 0 -1.0691 9.4 166.143)" clip-path="url(#gia)"><g fill-rule="evenodd"><path d="M107.96 30.661l-12.506-1.876-16.883-1.876-10.943-.312-10.629.312-8.13 2.502-7.19 7.815-5.628 15.945-1.25 3.44-7.504 2.5-4.377 7.191-3.126 10.317 3.44 9.067 8.128 2.814 6.565-3.127 3.127-6.878 3.752.626 1.25 1.563-1.25 7.19-.313 9.068 1.876 12.505-.074 7.143 5.701 9.114 10.005 7.19 17.508 7.504 19.383-2.814 16.883-12.193 7.817-12.505 5.002-9.067 1.25-22.51-3.752-19.384-6.877-17.195-6.566-9.066" fill="#f0d6b7"/><path d="M97.334-23.425l-44.709-1.876v-7.503l3.752-26.262-1.876-2.19-31.264 10.63-2.19 3.752-3.126 35.328-7.19 21.26-1.563 5.002 25.01 17.195 7.818 3.127 6.877-8.441 5.94-5.315 6.88-2.188 3.125-.938L68.57 1.899l2.814-3.44 7.19 2.502-5.002-9.693 27.2-12.818-3.439-1.876" fill="#335061"/><path d="M23.238 85.687l8.128 2.814 6.566-3.127 3.127-6.878 3.751.626.938 3.751-1.876 7.19 1.876 17.197-1.563 9.379 5.627 6.565 12.193 9.692-3.44 4.69-17.194-8.442-7.191-5.627-4.064-8.754-6.253-8.442-1.876-10.005 1.251-10.63" fill="#6d6b6d"/><path d="M36.055 115.07s4.69 11.567 23.448 17.195c18.759 5.628.938 4.065.938 4.065l-20.321-7.817-7.817-7.816-3.438-6.253 7.19.626M26.676 87.875s-6.566 21.886 18.446 25.012l-.938 3.752-17.195-4.065-5.003-16.257 1.251-10.63 3.439 2.188" fill="#dcd9d8"/></g><g fill="#f7e4cd"><path d="M36.681 58.799l4.094 3.966s1.847-.214 2.16-2.402c.312-2.19 1.25-21.886 14.693-32.516 1.227-.97-10.004 1.564-10.004 1.564L37.62 45.042M94.209 64.739s.729 9.477 3.28 8.748c2.553-.729 2.553-3.28 2.553-3.28s-6.198-4.01-5.833-5.468" fill-rule="evenodd"/><path d="M120.16 99.442s-5.153-1.088-5.628-5.628c-.474-4.54 5.628-.938 6.566-.625M82.327 99.129s-6.879-.938-6.879-5.314c0-4.378 7.817-4.065 10.005-2.19"/><g fill-rule="evenodd"><path d="M39.807 78.808s-11.881 7.191-13.131.312c-1.25-6.877-4.065-11.88 1.876-19.07l-4.064 1.25-3.752 9.691-1.25 9.38 7.19 7.504 8.129-.626 4.69-3.751.312-4.69M45.435 98.504s5.315 27.512 32.203 32.827c22.136 4.375 33.765-.938 38.142-5.94 0 0-19.696 23.447-38.455 16.257-18.759-7.191-32.514-20.322-32.202-28.762.532-14.377.313-14.382.313-14.382M117.97 122.27s-9.066.312-9.38-7.817c0 0 0-1.25.625-2.5 0 0 7.192 8.129 11.568 3.751"/><path d="M78.268 111.1s-1.56 12.477-12.199 5.223c-6.878-4.69-6.252-11.255-5.002-12.505s.91-3.77 1.862-2.04c.952 1.728.638 7.356 4.078 8.918 3.439 1.564 9.077 3.31 11.26.404"/></g></g><g fill="#49728b" fill-rule="evenodd"><path d="M48.874 26.597L19.486 13.466s12.193-48.46 5.94-63.467l-4.377 1.563-.313 18.446-8.128 35.015-3.44 9.692 30.639 20.633 9.067-8.753M51.896-.206l4.17-5.087v-18.76h-5.003s-.625 13.132-.625 14.696c0 1.563.624 7.19.624 7.19M52-26.866l-14.069-.625 4.065-2.813L52-31.868"/></g><g fill-rule="evenodd"><path d="M100.15-23.739l11.567.313 2.814-28.764-11.881-1.563-2.5 30.014" fill="#335061"/><path d="M103.27-23.739l17.508.938s7.19 18.133 7.19 19.07c0 .939 6.253 26.263 6.253 26.263l-14.069 14.694-2.813 2.501-7.504-7.503V3.148l-6.565-26.887" fill="#335061"/><path d="M111.09-21.55l-10.942-2.188 1.563-8.755c4.064-1.876 10.943 3.127 10.943 3.127M111.4 33.162l21.885-16.257.626 7.503-16.57 15.32-5.94-6.566" fill="#49728b"/><path d="M62.85-85.332l-6.473 26.266-3.22 19.38-.531 14.385 29.296 1.56 18.226.003-1.658-32.83 2.814-25.324-.312-4.69-23.76-1.876-14.382 3.126" fill="#fff"/><path d="M96.083-23.426s-1.563-32.515 3.127-55.65c0 0-9.38-5.94-23.136-7.503l26.262.938 3.126 1.875-3.752 51.273-.938 10.944" fill="#dcd9d8"/><path d="M115.06-49.691l12.193 3.44 23.135 1.25 3.44 10.629-6.254 18.446-7.19.938-10.005-3.127-9.599-4.686-5.095.935-3.972-1.56" fill="#fff"/><path d="M114.84-43.435s8.128 3.751 9.38 3.438L120.78-22.8l4.065 1.563s2.814-16.257 2.814-18.133c0 0 17.507-.938 19.07-.938 0 0 3.752 7.191 2.814 14.694l3.44-10.005.312-5.628-5.002-7.503-5.627-1.25-9.38.312-3.126 4.064-10.943-1.563-3.44-1.25" fill="#dcd9d8"/></g><path d="M102.56-21.241L95.682-3.733l-7.19 10.317s1.562 4.377 3.75 4.377h7.192l6.878-2.501-.625-11.568-3.127-18.134" fill="#fff"/><path d="M103.9-15.297S95.145 1.585 95.145 4.086c0 0 1.563 3.752 3.752 2.814 2.19-.938 6.879-3.439 6.879-3.439v5.94l-10.63 2.19-7.19-.939 12.193-28.763 2.5-.313" fill="#dcd9d8" fill-rule="evenodd"/><path d="M65.664 25.968l-8.661.942-8.13 2.501v-2.814l3.972-4.38 12.506-5.627" fill="#fff"/><path d="M51.689 25.031s9.693-4.065 12.819-3.127l.311-3.748-8.752 1.872-5.316 3.752.938 1.251" fill="#dcd9d8" fill-rule="evenodd"/><path d="M115.03 9.897c-5.305.156-10.098.786-14.294 1.97.285 1.72-.249 3.408.18 4.647 1.17.843 3.13.83 4.898 1.027-1.529.752-3.677 1.049-5.44.615-.042 1.194-.578 1.934-.902 2.868 2.982 1.064 10.024 8.044 13.984 5.732 1.887-1.099 2.689-7.377 2.835-10.43.122-2.533-.23-5.088-1.261-6.43" fill="#d33833" fill-rule="evenodd"/><path d="M115.03 9.897c-5.305.156-10.098.786-14.294 1.97.285 1.72-.249 3.408.18 4.647 1.17.843 3.13.83 4.898 1.027-1.529.752-3.677 1.049-5.44.615-.042 1.194-.578 1.934-.902 2.868 2.982 1.064 10.024 8.044 13.984 5.732 1.887-1.099 2.689-7.377 2.835-10.43.122-2.533-.23-5.088-1.261-6.43z" fill="none" stroke="#d33833" stroke-width="2"/><path d="M89.66 18.569c-.014-.401-.03-.806-.047-1.21-1.656-1.089-4.33-1.076-6.148-1.99 2.68-.117 4.79-.763 6.614-1.672l-.118-3.033c-3.036-2.078-5.81-5.173-9.384-7.122-1.69-.922-7.622-3.294-9.42-2.875-1.017.236-1.109 1.499-1.516 2.689-.866 2.548-2.861 6.605-3.035 10.44-.222 4.846-.71 12.967 4.51 11.969 4.213-.804 9.113-2.745 12.375-4.527 1.993-1.09 3.146-2.436 6.17-2.669" fill="#d33833" fill-rule="evenodd"/><path d="M89.66 18.569c-.014-.401-.03-.806-.047-1.21-1.656-1.089-4.33-1.076-6.148-1.99 2.68-.117 4.79-.763 6.614-1.672l-.118-3.033c-3.036-2.078-5.81-5.173-9.384-7.122-1.69-.922-7.622-3.294-9.42-2.875-1.017.236-1.109 1.499-1.516 2.689-.866 2.548-2.861 6.605-3.035 10.44-.222 4.846-.71 12.967 4.51 11.969 4.213-.804 9.113-2.745 12.375-4.527 1.993-1.09 3.146-2.436 6.17-2.669z" fill="none" stroke="#d33833" stroke-width="2"/><path d="M92.675 12.788c-.463 2.64-.999 3.393-.792 5.695 7.04 4.693 8.361-8.061.792-5.695" fill="#d33833" fill-rule="evenodd"/><path d="M92.675 12.788c-.463 2.64-.999 3.393-.792 5.695 7.04 4.693 8.361-8.061.792-5.695z" fill="none" stroke="#d33833" stroke-width="2"/><path d="M102.87 10.649s-2.19 3.127-.626 4.065c1.564.938 3.127 0 4.065 1.563s0 2.501.313 4.377 1.877 2.189 3.44 2.501c1.562.313 5.94.938 6.565-.625l-1.876 5.627-3.752 1.25-11.88-6.877-.626-3.44v-6.877M70.041.331c-.376 4.88-.773 9.752-1.215 14.626-.662 7.279 1.748 6.009 8.057 6.009.964 0 5.933-1.15 6.289-1.876 1.705-3.483-2.851-2.709 1.964-5.335 4.065-2.216 11.246 1.346 9.603 6.273-.919 1.095-4.789.341-6.176 1.06l-7.327 3.8c-3.108 1.612-10.29 3.962-13.603 1.709-8.395-5.71.53-19.974 3.524-25.93" fill="#ef3d3a" fill-rule="evenodd"/><g fill="#231f20" fill-rule="evenodd"><path d="M78.268 111.1c-8.521 1.985-12.755-3.566-15.338-9.323-2.306.559-1.389 3.695-.806 5.294 1.525 4.194 7.672 9.778 12.694 9.02 2.161-.325 5.086-2.301 3.45-4.99M119.79 101.4l.404-.016c1.926-4 3.593-8.238 6.022-11.769-1.628-3.79-12.322-7.144-12.157-.338 2.313 1.01 6.305.206 8.356 1.497-1.186 3.254-2.897 6.024-2.625 10.626M82.63 101.29c1.827-3.35 2.422-6.868 5.019-9.4 1.17-1.14 3.444-2.529 2.316-5.698-.263-.747-2.189-2.414-3.3-2.741-4.06-1.2-13.521-.248-10.317 4.814 3.358-.157 7.871-2.18 10.38.257-1.927 3.081-5.363 9.177-4.098 12.768M118.26 67.253c-6.113-3.927-12.93-8.197-22.947-7.207-2.14 1.86-2.956 6.002-.877 8.737 1.082-1.861.402-5.284 3.419-5.799 5.684-.972 12.299 3.477 16.387 5.032 2.535 4.275-.219 5.847-2.503 8.597-4.675 5.636-10.947 12.622-10.72 21.06 1.89 1.37 2.053-2.092 2.325-2.722 2.44-5.714 8.585-13.021 13.07-17.912 1.1-1.205 2.914-2.36 3.115-3.157.582-2.315-1.513-5.09-1.27-6.63M37.668 71.387c-1.916 1.094-2.372 5.91-4.622 6.048-3.215.195-2.629-6.25-2.616-10.018-2.213 2.009-2.602 8.194-.976 11.37-1.853.91-2.68-1.003-3.708-1.677 1.32 9.595 14.036 4.45 11.922-5.723M122.15 63.257c-2.846-5.417-6.871-11.382-15.222-11.555-.17 1.75-.3 4.411.009 5.464 6.384.614 10.325 3.863 15.212 6.091M82.149 59.745c5.326-2.8 15.114-3.102 22.353-2.89.388-1.586.379-3.545.394-5.48-9.305-.463-20.307 1.84-22.747 8.37M81.136 54.523c3.683-9.247 16.341-8.182 27.016-7.927-.47-1.2-1.489-2.62-2.755-3.132-3.42-1.392-12.855-2.448-17.604.074-3.011 1.601-4.946 5.219-6.596 7.34-.797 1.024-4.765 3.64-.06 3.645"/></g><path d="M117.82 3.516c-4.322-7.402-8.457-15.005-13.585-21.534 2.15 6.32 3.07 16.9 3.394 24.965 4.498 2.105 8.349-.474 10.191-3.43" fill="#81b0c4" fill-rule="evenodd"/><g fill="#231f20" fill-rule="evenodd"><path d="M141.07-23.089c-4.839-.969-8.239-5.671-12.959-5.37 2.594 3.658 7.14 5.2 12.959 5.37M143.21-30.661c-3.944-.417-8.576-1.055-12.577-.726 1.894 2.892 9.19 1.894 12.577.726M144.58-37.19c-4.433-.096-9.942-.008-14.155.346 2.492 2.677 11.28.993 14.155-.346"/></g><g fill-rule="evenodd"><path d="M109.48-55.057c.636-5.567 2.843-11.207 2.566-17.304-2.45-.827-3.858-1.55-7.142-1.545-.232 5.181-.925 13.102-.718 18.041 1.615-.107 3.997 1.154 5.294.808" fill="#dcd9d8"/><path d="M102.33 26.985c-2.226-1.453-4.121-3.267-6.259-4.818-4.74-.235-7.327.328-10.81 3.05.057.219.407.121.42.39 5.075-2.262 11.524.92 16.648 1.378" fill="#f0d6b7"/><path d="M75.694-7.603c1.394 6.04 6.857 9.17 11.817 12.497 5.12-6.498 8.234-14.855 11.663-22.92-8.102 2.443-16.38 6.406-23.481 10.423" fill="#81b0c4"/><path d="M104.18-55.865c-.207-4.94.486-12.86.718-18.041 3.283-.004 4.691.718 7.142 1.545.276 6.096-1.93 11.737-2.566 17.304-1.298.346-3.679-.914-5.294-.808zm-51.13 28.09c2.165-19.906 5.301-36.639 11.054-54.266 12.766-3.876 28.157-4.214 39.441-.716-2.072 9.948-1.167 22.06-2.378 32.677-.912 7.98-.447 16.009-1.698 24.15-13.673 2.844-33 .665-46.418-1.845zm49.651 1.72c-.115-8.549.383-16.982 1.036-25.542 3.282.493 5.51.822 8.56 1.49-.99 8.241-.869 17.514-2.886 24.804-2.332-.023-4.385.027-6.71-.752zm16.653 1.378c-1.558.357-3.372.014-4.86-.015.7-6.969 2.397-14.659 2.995-21.974 2.342-.073 3.593 1.032 5.52 1.403.102 6.421-.562 15.268-3.655 20.586zm25.215-23.038c4.882 1.186 7.952 7.165 6.586 13.305-.916 4.127-2.548 11.898-4.295 14.538-1.29 1.953-4.79 4.51-7.584 2.72-4.545-2.91-12.552-3.755-15.867-7.278 1.662-5.534 2.178-13.135 2.864-20.146 5.678-.354 12.665 1.562 17.387-.471-3.297-1.068-7.575-1.077-10.423-2.633 2.328-1.125 7.778-.897 11.332-.035zM99.17-18.025c-3.43 8.063-6.543 16.42-11.663 22.918-4.96-3.327-10.423-6.456-11.817-12.497 7.1-4.017 15.379-7.98 23.481-10.422zm8.453 24.971c-.325-8.065-1.245-18.644-3.395-24.965 5.128 6.53 9.263 14.132 13.585 21.534-1.842 2.957-5.693 5.536-10.19 3.431zm-9.582 3.405c-1.943.21-3.592-2.233-6.117-1.177-.58-.64-1.105-1.333-1.695-1.958 5.579-6.723 8.114-16.262 12.423-24.163 2.312 7.59 2.045 15.904 2.555 24.188-3.177-.201-4.94 2.873-7.166 3.11zm-6.161 8.132c-.208-2.303.328-3.056.791-5.695 7.57-2.367 6.248 10.388-.791 5.695zm-8.394 2.755c-3.261 1.782-8.161 3.723-12.374 4.527-5.222.999-4.732-7.123-4.51-11.968.173-3.836 2.168-7.893 3.035-10.441.406-1.19.498-2.453 1.515-2.69 1.798-.418 7.73 1.954 9.42 2.875 3.575 1.95 6.348 5.045 9.384 7.123.04 1.011.078 2.021.119 3.032-1.826.91-3.935 1.555-6.615 1.673 1.818.914 4.492.901 6.148 1.989.016.405.033.81.047 1.21-3.024.234-4.176 1.58-6.17 2.67zm-31.152 5.659c-2.707-2.748 7.592-6.494 10.871-6.696-.018 1.739.991 3.378.788 4.626-3.895.684-9.013.232-11.66 2.07zm33.345-1.29c-.013-.27-.363-.172-.42-.39 3.482-2.722 6.07-3.285 10.81-3.05 2.137 1.551 4.033 3.365 6.259 4.818-5.124-.458-11.574-3.64-16.648-1.379zm30.606-9.282c-.146 3.053-.948 9.332-2.835 10.431-3.961 2.312-11.002-4.668-13.984-5.732.324-.934.86-1.674.901-2.868 1.764.434 3.912.137 5.44-.615-1.767-.198-3.727-.185-4.897-1.027-.429-1.239.105-2.927-.18-4.647 4.196-1.184 8.989-1.814 14.294-1.97 1.032 1.341 1.383 3.896 1.261 6.429zM47.777 24.24c-.85.606-6.6 8.087-7.388 7.777-10.405-4.103-20.134-11.199-28.828-17.91 8.29-17.787 11.635-39.579 12.227-60.582 9.496-4.441 17.836-10.844 30.722-11.512-1.491 10.55-2.852 19.962-3.699 29.895-3.237 1.365-7.882-.062-10.913.423-.025 3.651 4.628 1.6 5.015 4.054.292 1.858-2.56 1.998-1.631 4.923 2.368-.861 3.612-2.763 6.138-3.477 2.309 5.05-.032 13.985.3 18.205.064.792.397 4.39 2.172 3.759 1.57-.559-.09-9.569.082-13.563.157-3.68-.444-7.242 1.046-9.552a355.817 355.817 0 0 0 38.576 3.16c-2.964 1.272-6.485 2.475-10.345 4.651-2.093 1.18-8.69 3.635-9.293 5.622-.964 3.167 2.528 4.855 3.125 7.57-6.285-3.428-7.511 3.286-8.998 8.042-1.347 4.308-2.114 7.526-2.445 10.01-5.414 2.581-11.203 5.195-15.863 8.505zm63.009 6.872c8.67 4.204 10.232-15.711 6.834-22.127.525-1.914 2.331-2.646 3.069-4.366-4.838-8.667-10.211-16.756-15.148-25.32 3.672 2.286 8.917.409 13.238 2.12 1.58.624 2.722 4.24 3.918 7.133 3.29 7.958 6.743 17.99 8.28 25.586.346 1.73 1.292 5.5 1.08 7.04-.378 2.758-4.12 4.803-6.022 6.508-3.506 3.15-5.714 5.921-9.371 8.866-1.483-2.189-4.666-3.66-5.878-5.44zM27.95 107.99c-4.13-4.545-3.266-13.062-2.766-19.121 7.467 4.697 17.377-.372 17.284-8.36 3.565.094 1.332 4.452.687 7.259-2.107 9.169 3.55 19.13.256 27.516-6.395-.485-11.649-3.097-15.46-7.294zm29.558 26.38c-9.352-2.65-21.337-9.446-25.18-17.847 2.976.432 5.041 1.933 7.977 2.119 1.11.072 2.563-.466 3.838-.148 2.54.63 4.685 6.327 6.602 8.447 1.868 2.07 4.114 2.954 5.651 4.841.988.477 2.448.444 2.504 1.927-.428.457-.879.806-1.392.66zm48.681-2.493c-9.707 5.477-26.136 9.596-36.462 4.449-8.331-4.155-19.593-11.027-23.433-19.737 3.587-8.405-1.062-16.106-1.36-24.64-.157-4.54 2.139-8.504 2.315-13.446-1.228-2.025-4.978-2.275-7.574-2.136-.873 4.372-2.403 9.287-6.906 9.78-6.371.697-11.03-4.576-11.319-10.085-.342-6.48 4.978-17.22 12.517-16.475 2.913.287 3.629 3.207 6.802 3.177 1.72-3.432-2.653-4.51-3.103-6.964-.117-.634.363-3.112.642-4.274 1.37-5.658 4.422-12.982 7.427-17.29 3.814-5.464 11.307-6.288 19.37-6.823 1.44 3.101 6.743 2.846 10.2 2.035-4.143 1.64-7.993 5.617-11.185 9.137-3.665 4.039-7.378 8.371-7.566 13.65 6.927-9.61 12.65-18.003 25.246-22.23 9.53-3.196 20.662 1.465 27.986 6.608 3.039 2.137 4.853 5.529 7.013 8.634 8.082 11.626 11.854 28.219 11.024 44.303-.342 6.633-.327 13.244-2.552 17.706-2.326 4.666-10.193 8.84-14.8 4.62-.853 4.537 3.83 7.344 9.331 5.71-3.922 5.063-8.039 11.145-13.614 14.29zm18.084-149.66c7.585 3.77 21.757 10.149 26.512-.014 1.755-3.746 3.814-10.079 4.723-13.946 1.284-5.456-1.392-16.923-7-18.754-4.953-1.617-10.733-1.518-16.7-.32-.702.585-1.484 1.603-2.03 2.665-4.261.165-8.25-.229-11.615-1.98.319-3.15-1.812-3.656-3.81-4.305-1.48-5.872 2.963-13.541 1.9-18.896-.76-3.815-5.453-4.405-8.902-5.118-.113-2.12.15-3.89.386-5.683-.789-2.907-4.327-4.561-7.679-4.967-11.029-1.326-27.775-1.922-38.384 1.893-2.96 7.261-5.292 16.093-7.758 24.384-10.346-1.105-18.715 4.464-26.603 8.113-2.731 1.266-6.51 1.964-7.53 4.138-.99 2.105-.584 6.14-.83 9.95-.625 9.733-1.16 19.12-3.73 29.086-1.154 4.472-3.165 8.418-4.568 12.727C9.358 5.184 7.092 10.12 6.5 14.1c-.877 5.903 4.681 6.232 8.235 8.79 5.494 3.954 9.806 6.142 15.756 9.711 1.762 1.057 7.077 3.733 7.681 4.966 1.202 2.443-2.062 5.888-2.935 7.803-1.38 3.03-2.1 5.602-2.298 8.59-4.992.789-8.775 3.76-11.06 7.109-3.781 5.543-6.403 15.798-3.132 23.599.257.614 1.536 1.822 1.725 2.765.372 1.858-.7 4.329-.768 6.305-.343 10.14 1.716 18.875 8.541 21.932 2.771 11.038 12.688 14.71 22.032 20.195 3.493 2.05 7.343 3.36 11.32 4.824 14.263 5.25 36.15 4.261 47.987-4.692 5.02-3.797 13.044-11.813 15.914-17.617 7.58-15.323 7.042-40.931 1.74-59.571-.712-2.503-1.746-6.181-3.19-9.187-1.006-2.1-4.134-6.3-3.754-8.153.391-1.916 7.132-7.034 8.577-8.428 2.603-2.51 7.548-5.843 7.948-9.012.43-3.372-1.485-7.984-2.456-11.238-3.245-10.858-6.412-20.895-10.091-30.576" fill="#231f20"/><path d="M73.674 57.38c.411.548 2.674 1.38 5.84-.144 0 0-3.752-.626-3.44-6.881l-1.564.313s-1.615 5.672-.836 6.712" fill="#f7e4cd"/><path d="M101.09 3.617a1.72 1.72 0 1 0-3.44.001 1.72 1.72 0 0 0 3.44-.001M102.81-4.355a1.72 1.72 0 1 0-3.44 0 1.72 1.72 0 0 0 3.44 0" fill="#1d1919"/></g><g><rect transform="matrix(.8 0 0 -.8 0 144)" x="16.854" y="177.38" width="70.412" height="4.12" rx=".983" ry=".983"/><rect transform="scale(1 -1)" x="78.502" y="-2.097" width="50.037" height="3.296" rx=".786" ry=".786"/><rect transform="scale(1 -1)" x="13.483" y="-3.697" width="54.831" height="3.296" rx=".786" ry=".786"/><rect transform="scale(1 -1)" x="83.296" y="-3.697" width="45.243" height="3.296" rx=".786" ry=".786"/></g></g></symbol><symbol viewBox="0 0 24 24" id="json" xmlns="http://www.w3.org/2000/svg"><path d="M5 3h2v2H5v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5h2v2H5c-1.07-.27-2-.9-2-2v-4a2 2 0 0 0-2-2H0v-2h1a2 2 0 0 0 2-2V5a2 2 0 0 1 2-2m14 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3h2m-7 12a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m-4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#fbc02d"/></symbol><symbol viewBox="0 0 50 50" id="julia" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -247)" stroke-width="5.673"><circle cx="13.497" cy="281.632" r="9.555" fill="#bc342d"/><circle cx="36.081" cy="281.632" r="9.555" fill="#864e9f"/><circle cx="24.722" cy="262.389" r="9.555" fill="#328a22"/></g></symbol><symbol viewBox="0 0 64 64" id="karma" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -233)"><path d="M38.556 288.413l-20.29-26.687 9.532-7.246 20.29 26.686h-.001.002l5.527 7.247z" fill="#359b8b" stroke-width=".173"/><path d="M35.681 241.172L24.92 255.327v-14.13H12.947v13.817l7.84 33.235h4.132v-13.147l.003.003 20.29-26.686-.008-.006 5.504-7.24H35.84v.12z" fill="#3cbeae" stroke-width=".206"/></g></symbol><symbol viewBox="0 0 24 24" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7 14a2 2 0 0 1-2-2 2 2 0 0 1 2-2 2 2 0 0 1 2 2 2 2 0 0 1-2 2m5.65-4A5.99 5.99 0 0 0 7 6a6 6 0 0 0-6 6 6 6 0 0 0 6 6 5.99 5.99 0 0 0 5.65-4H17v4h4v-4h2v-4H12.65z" fill="#26a69a"/></symbol><symbol viewBox="0 0 24 24" id="kivy" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(1.89 0 0 1.89 -12.157 -11.429)" fill="#90a4ae"><path d="M7.026 8.63v4.474l1.928-1.928a.437.437 0 0 0 0-.619zM9.38 16.072v-4.474l-1.927 1.927a.437.437 0 0 0 0 .62zM18.576 10.412l-5.346.564-.017.018 2.39 2.39zM9.922 8.502s.023 3.304-.003 4.452c-.02.856.371 1.114.746 1.507.538.564 1.599 1.57 1.599 1.57a.53.53 0 0 0 .75 0l1.843-1.844a.53.53 0 0 0 0-.75z"/></g></symbol><symbol viewBox="0 0 24 24" id="kl" xmlns="http://www.w3.org/2000/svg"><defs><style>.a{fill:#3aaae1}.b{fill:#fdfeff}</style></defs><title>kl</title><path d="M12.033 1.737c-.25-.003-.5.11-.729.337C8.225 5.15 5.15 8.227 2.078 11.31c-.144.144-.229.346-.341.521v.41c.16.223.294.474.485.666a3259.51 3259.51 0 0 0 8.936 8.937c.193.192.443.325.666.486h.41c.205-.142.436-.256.609-.428 3.046-3.041 6.09-6.083 9.133-9.127.47-.47.472-1.005.006-1.472l-9.218-9.217c-.23-.23-.48-.347-.731-.35zm-1.062 4.545l1.386.832c.702.422 1.403.846 2.109 1.262a.544.544 0 0 1 .04.026l.016.013.017.013c.061.056.089.123.088.224a510.281 510.281 0 0 0 0 3.794.463.463 0 0 1-.007.094c-.015.069-.054.103-.142.109a.464.464 0 0 1-.044.002c-.045-.002-.09-.002-.136-.003-.323-.006-.648-.001-.998-.001v-.527-1.34-.671-.003l.004-.668c0-.147-.039-.231-.17-.308-.893-.528-1.78-1.066-2.67-1.6-.051-.03-.101-.065-.173-.111l.001-.003h-.001zm.362 3.39c.068-.003.119.043.173.138.085.148.174.293.264.44l.015.025c.096.154.194.31.292.47l-1.915 1.176c-.337.207-.673.417-1.014.617-.113.067-.154.143-.154.277.01.977.01 1.955.014 2.932V16H7.7V16h-.002c-.004-.053-.014-.112-.014-.17-.005-1.25-.006-2.501-.015-3.751 0-.142.045-.222.164-.294a467.13 467.13 0 0 0 3.353-2.054l.016-.01a.606.606 0 0 1 .032-.017l.016-.008a.308.308 0 0 1 .033-.013l.012-.004a.157.157 0 0 1 .028-.005l.01-.001zm5.677 3.126l.314.54.346.594v.001c-.158.094-.298.178-.438.259l-3.097 1.798c-.106.062-.189.071-.3.01l-.893-.496-1.524-.843-.895-.493c-.035-.02-.068-.044-.129-.085h.001l.137-.25.495-.902 1.446.795c.442.243.886.483 1.323.734.121.07.212.072.334 0 .894-.525 1.792-1.043 2.689-1.563.057-.034.118-.061.191-.1z" fill="#29b6f6" stroke-width=".041"/></symbol><symbol viewBox="0 0 24 24" id="kotlin" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="gpb"><stop offset="0" stop-color="#cb55c0"/><stop offset="1" stop-color="#f28e0e"/></linearGradient><linearGradient id="gpa"><stop offset="0" stop-color="#0296d8"/><stop offset="1" stop-color="#8371d9"/></linearGradient><linearGradient xlink:href="#gpa" id="gpc" x1="1.725" y1="22.67" x2="22.185" y2="1.982" gradientUnits="userSpaceOnUse" gradientTransform="translate(1.638 1.155) scale(.89324)"/><linearGradient xlink:href="#gpb" id="gpd" x1="1.869" y1="22.382" x2="22.798" y2="3.377" gradientUnits="userSpaceOnUse" gradientTransform="translate(1.638 1.155) scale(.89324)"/></defs><path d="M3.307 3.003v18.048h18.05v-.03L16.88 16.51l-4.48-4.515 4.48-4.515 4.443-4.477H3.307z" fill="url(#gpc)"/><path d="M12.538 3.003l-9.23 9.23v8.818h.083l9.032-9.032-.025-.024 4.48-4.515 4.444-4.477h-8.784z" fill="url(#gpd)"/></symbol><symbol viewBox="0 0 240 240" id="laravel" xmlns="http://www.w3.org/2000/svg"><path d="M216.05 119.036c-1.433.343-24.945 6.673-24.945 6.673l-19.227-28.622c-.537-.828-.99-1.656.359-1.849 1.345-.196 23.195-4.477 24.182-4.723.99-.245 1.837-.536 3.053 1.267 1.21 1.8 17.836 24.626 18.464 25.506.627.877-.447 1.41-1.883 1.748m-4.101 49.326c.588 1.003 1.176 1.64-.67 2.367-1.843.73-62.243 22.847-63.418 23.39-1.173.546-2.092.73-3.607-1.637-1.51-2.362-21.16-39.264-21.16-39.264l64.03-18.075c1.876-.644 2.317-.405 3.103.822 1.074 1.68 21.143 31.403 21.726 32.4m-103.7-21.087c-.78.202-37.566 9.733-39.525 10.22-1.965.485-1.965.246-2.188-.49-.226-.727-43.728-98.053-44.333-99.271-.605-1.214-.574-2.177 0-2.177.571 0 34.734-3.313 35.944-3.383 1.207-.07 1.08.205 1.526 1.033l49.025 91.818c.84 1.58 1.239 1.81-.452 2.248m94.588-59.77c-3.5-4.58-5.2-3.751-7.357-3.41-2.154.336-27.277 4.915-30.194 5.449-2.918.536-4.758 1.803-2.963 4.53 1.597 2.422 18.113 27.824 21.751 33.42l-65.663 17.066L66.18 49.832c-2.075-3.342-2.507-4.514-7.236-4.28-4.735.23-40.969 3.495-43.55 3.731-2.58.233-5.416 1.479-2.835 8.09 2.583 6.612 43.734 102.82 44.88 105.62 1.149 2.803 4.128 7.345 11.11 5.527 7.157-1.871 31.969-8.894 45.52-12.742 7.163 14.07 21.77 42.619 24.473 46.707 3.607 5.459 6.089 4.56 11.626 2.738 4.325-1.42 67.65-26.129 70.502-27.4 2.855-1.273 4.613-2.184 2.685-5.275-1.419-2.28-18.124-26.558-26.876-39.26 5.993-1.733 27.305-7.888 29.575-8.557 2.646-.779 3.008-2.19 1.572-3.94-1.436-1.755-21.293-28.72-24.79-33.296z" fill="#ff5722" stroke="#ff5722" stroke-width="8.852" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 24 24" id="less" xmlns="http://www.w3.org/2000/svg"><path d="M13.696 2.999V5h2.002v5a2 2 0 0 0 1.999 2 2 2 0 0 0-2 2v5h-2v2h2a2 2 0 0 0 2-2v-4a2 2 0 0 1 2-2h1V11h-1a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2.001zm-.03 12.766v.47a1 1 0 0 0 .03-.236 1 1 0 0 0-.03-.234zM10.566 21v-2.001H8.565v-5a2 2 0 0 0-2-2 2 2 0 0 0 2-2V5h2.001v-2H8.565a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-.999V13h1a2 2 0 0 1 2 2v3.999A2 2 0 0 0 8.564 21zm.03-12.766v-.47a1 1 0 0 0-.03.236 1 1 0 0 0 .03.234z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="lib" xmlns="http://www.w3.org/2000/svg"><path d="M19 7H9V5h10m-4 10H9v-2h6m4-2H9V9h10m1-7H8a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2M4 6H2v14a2 2 0 0 0 2 2h14v-2H4V6z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 40 40" id="livescript" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -257)" fill="#317eac"><path stroke-width="3.299" d="M5.419 260.18h3.685v34.207H5.419z"/><path stroke-width="3.299" d="M37.074 288.197v3.685H2.867v-3.685z"/><path stroke-width="2.894" d="M29.612 265.658l2.004 2.005L7.428 291.85l-2.004-2.005z"/><path stroke-width="2.325" d="M10.73 262.471h2.835v22.08H10.73z"/><path stroke-width="2.063" d="M15.36 262.519h2.835v17.382H15.36z"/><path stroke-width="1.77" d="M19.99 262.471h2.835v12.802H19.99z"/><path stroke-width="1.422" d="M24.526 262.491h2.835v8.254h-2.835z"/><path stroke-width="1.128" d="M28.783 262.463h2.835v5.197h-2.835z"/><path stroke-width="2.325" d="M34.801 286.545v-2.835h-22.08v2.835z"/><path stroke-width="2.063" d="M34.753 281.914v-2.835H17.371v2.835z"/><path stroke-width="1.77" d="M34.801 277.284v-2.835H21.999v2.835z"/><path stroke-width="1.422" d="M34.781 272.749v-2.835h-8.254v2.835z"/><path stroke-width="1.128" d="M34.809 268.492v-2.835h-5.197v2.835z"/></g></symbol><symbol viewBox="0 0 24 24" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M12 17a2 2 0 0 0 2-2 2 2 0 0 0-2-2 2 2 0 0 0-2 2 2 2 0 0 0 2 2m6-9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2h1V6a5 5 0 0 1 5-5 5 5 0 0 1 5 5v2h1m-6-5a3 3 0 0 0-3 3v2h6V6a3 3 0 0 0-3-3z" fill="#ffd54f"/></symbol><symbol viewBox="0 0 24 24" id="lua" xmlns="http://www.w3.org/2000/svg"><circle cx="12.203" cy="12.102" r="10.322" fill="none" stroke="#42a5f5"/><path d="M12.33 5.746a6.483 6.381 0 0 0-6.482 6.381 6.483 6.381 0 0 0 6.482 6.38 6.483 6.381 0 0 0 6.484-6.38 6.483 6.381 0 0 0-6.484-6.38zm1.86 1.916a2.329 2.292 0 0 1 2.33 2.293 2.329 2.292 0 0 1-2.33 2.291 2.329 2.292 0 0 1-2.329-2.29 2.329 2.292 0 0 1 2.328-2.294z" fill="#42a5f5" fill-rule="evenodd"/><ellipse cy="4.615" cx="19.631" rx="2.329" ry="2.292" fill="#42a5f5" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 24 24" id="markdown" xmlns="http://www.w3.org/2000/svg"><path d="M2 16V8h2l3 3 3-3h2v8h-2v-5.17l-3 3-3-3V16H2m14-8h3v4h2.5l-4 4.5-4-4.5H16V8z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" preserveAspectRatio="xMidYMid" id="markojs" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -120.96)" stroke-width=".984"><path d="M4.002 126.482c-.655 1.07-1.32 2.14-1.976 3.21-.655 1.06-1.308 2.142-1.963 3.212l.002.002-.002.002c.655 1.07 1.308 2.15 1.963 3.211.655 1.07 1.32 2.141 1.976 3.211h3.33c-.664-1.07-1.318-2.14-1.974-3.21-.653-1.069-1.307-2.145-1.961-3.214.654-1.068 1.308-2.146 1.961-3.215a601.93 601.93 0 0 1 1.974-3.209z" fill="#2196f3"/><path d="M3.999 126.482l-.002.002c.655 1.07 1.31 2.15 1.964 3.212.655 1.07 1.32 2.14 1.974 3.21h3.331c-.664-1.07-1.319-2.14-1.974-3.21-.653-1.068-1.306-2.146-1.96-3.214z" fill="#26a69a"/><path d="M15.203 126.482l.002.002c-.655 1.07-1.31 2.15-1.965 3.212-.655 1.07-1.319 2.14-1.974 3.21h-3.33c.664-1.07 1.318-2.14 1.973-3.21.654-1.069 1.307-2.146 1.961-3.214z" fill="#8bc34a"/><path d="M11.874 126.484c.664 1.07 1.318 2.14 1.974 3.21.653 1.068 1.307 2.146 1.961 3.214-.654 1.069-1.308 2.145-1.961 3.213-.656 1.07-1.31 2.14-1.974 3.21h3.33c.655-1.07 1.319-2.14 1.974-3.21.655-1.06 1.31-2.14 1.966-3.21l-.002-.003.002-.002c-.656-1.07-1.311-2.152-1.966-3.213-.655-1.07-1.319-2.138-1.974-3.209z" fill="#ffc107"/><path d="M16.74 126.482c.665 1.07 1.319 2.14 1.974 3.21.654 1.068 1.306 2.146 1.96 3.214-.654 1.069-1.306 2.145-1.96 3.213-.655 1.07-1.31 2.141-1.974 3.211h3.33c.656-1.07 1.32-2.14 1.974-3.21.655-1.062 1.31-2.141 1.966-3.212l-.002-.002.002-.002c-.655-1.07-1.31-2.152-1.966-3.213-.655-1.07-1.318-2.138-1.973-3.209z" fill="#f44336"/></g></symbol><symbol viewBox="0 0 23 24" id="mathematica" xmlns="http://www.w3.org/2000/svg"><path d="M11.512 1.523l-.073.025-.46.794-.454.763-1.217 2.09H9.29L5.435 3.5l-.1-.047h-.018v.092l.025.163v.086l.132 1.226v.082l.032.252v.082l.22 2.137v.075l.018.082v.06l-2.348.507-.04.015-.457.1-.025.01h-.042l-1.096.244-.04.007-.17.036v.082l.018.01 1.859 2.086.053.052.114.132.804.909v.005l-.053.05-.22.257-2.564 2.875-.01.007v.082l.071.006.295.075 1.697.366v.006l2.139.472h.015v.047l-.036.252v.08l-.046.412v.082l-.036.244v.082l-.045.412v.08l-.05.41v.08l-.036.244v.082l-.046.412v.082l-.05.407v.082l-.032.248V20l-.05.407v.104h.037l3.642-1.6.294-.134h.018l.177.312.539.911.015.032.854 1.465.16.262.404.695.007.022h.092l.005-.022.017-.025.56-.947.014-.042.6-1.033.316-.539.644-1.091.05.013 3.906 1.721h.035v-.085l-.138-1.32v-.082l-.032-.244v-.082l-.035-.245v-.085l-.033-.244v-.081l-.032-.245v-.082l-.032-.244v-.085l-.035-.245v-.082l-.032-.245v-.082l-.033-.244v-.085l-.025-.17v-.053l1.632-.354.043-.008.458-.107h.028v-.01l.23-.05.03-.01h.042l.382-.09.025-.01h.043l.194-.05h.033l1.015-.23.07-.007v-.064l-.015-.013-1.19-1.342-.028-.028-.197-.22-1.428-1.604v-.006l.295-.323.4-.457 2.148-2.408.015-.01v-.065l-.035-.008-1.288-.28-.372-.084-.047-.01-2.481-.544v-.045l.432-4.265v-.02h-.042l-.302.135-.01.014h-.025l-3.307 1.45-.297.135h-.015l-2.028-3.483-.099-.145-.014-.045zm-.001 1.114l1.365 2.323.34.592-.008.025-1.18 1.511-.517.66-.012-.01-.258-.335-.04-.05-1.397-1.787.03-.063 1.378-2.365.287-.491zm4.908 2.039l-.007.025-.168.225-.538.066zm-9.817.004l.053.02.677.3h-.499l-.224-.3zM16.947 5l-.123 1.248-.113-.928.226-.307zm-9.26.156l.053.024.705.309-.757-.175zm7.388.116l.02.168-1.318.403.003-.003.16-.071 1.015-.444zM9.669 6.388l.944 1.204v.01L9.483 7.2zm3.55.172l.21.682-.234.084-.089.022-.702.255.008-.022.776-.982zm-5 .836l.986.356.898.312.048.02 1.054.373.011 3.086-.362-.117-.67-.224-.081-.038-.735-.245-.77-.256-.29-.1-.011-.255-.032-1.195-.01-.287-.015-.894-.013-.297zm6.583 0l-.011.227-.028.9-.008.303-.032 1.475-.01.262-.337.117-.734.245-.77.256-.712.245-.355.117.01-3.086 1.632-.578zm.585.437l.09.735.79-.097-.915 1.302-.018.006.01-.183.018-.877zm-9.451.536l.152.22 1.447 2.049-2.607.968-.05.015-1.972-2.214-.28-.312.003-.01.115-.018.424-.1.14-.021.337-.078.042-.01zm11.146.003l3.284.713.029.01-.022.025-1.954 2.192-.277.312-.092-.036-2.564-.95.475-.681.152-.216zM6.787 8.52h.86l.036 1.258-.013-.006-.763-1.078zm1.358 2.625l.152.06.77.252.712.245.746.247.49.167-.065.092-1.723 2.334-1.015-.302-.082-.017-.035-.015-1.902-.56.938-1.22.981-1.277zm6.73 0l.033.006 1.787 2.327.132.17-.128.036-.032.014-2.196.642-.105.032-.564.17-.018-.003-1.053-1.44-.174-.239-.547-.726-.007-.018.469-.16.769-.254.713-.245.77-.252zm-7.766.305l-.007.02-.405.523-.291-.291.657-.245zm8.802 0l.043.007.578.212.714.27-.661.394-.375-.479-.03-.042-.262-.342zm-10.843.75l-.67.668.355-.397.207-.23zm12.911.016l.068.025.045.042.554.627.042.043.204.228-.255.135zm-6.473.265l.022.015 1.38 1.872.032.05.343.465.008.031-.088.117-.422.629-.047.074-.245.343-.97 1.43-.013.007-1.18-1.72-.096-.16-.493-.708-.008-.037 1.618-2.191.007-.01zm7.827 1.194l.565.633.063.082-.272-.093-.037-.013zm-15.785.148l.297.299-.637.218-.152.05.038-.058zm13.224.47l-.855.448.346.66-.185-.058-.27-.088-1.092-.348.012-.01zm-9.687.255l1.222.356-.006.007-.458.145-.443.135-.032.01-.49.157zm-2.765.048l.318.32 2.007.517-.567.18-.055.004-2.103-.469-.744-.156.007-.006zm14.966.205l.548.188v.003l-.457.1-.043.014-1.069.23zm-10.23.507l.007.227.01.347.025 1.363.025.691-.007.255-.24.107-2.863 1.255.032-.372.033-.255.017-.227.031-.256.037-.407.045-.42.018-.23.032-.251.032-.412.05-.414.013-.14 1.455-.457.003-.014.301-.098zm4.908 0l1.245.39v.014l.312.1 1.146.362.022.23.03.255.043.408.04.42.017.23.033.251.032.412.042.325.078.848-.078-.04-3.025-1.322-.004-.305.06-2.368zm-4.295.617l.015.007.067.107.6.875-.64.531-.034-1.438zm3.671 0h.008l-.005.06-.02.678-.005.214-.479-.223zm-2.888 3.605l.763.915.001.37-.017-.006-.025-.05-.464-.791-.012-.018zm1.53.61l.184.083-.343.586-.018.007.002-.532z" fill="#f44336" fill-rule="evenodd" stroke="#f44336" stroke-width=".7747499999999999" stroke-linejoin="round"/></symbol><symbol viewBox="0 0 720 720" id="matlab" xmlns="http://www.w3.org/2000/svg"><title>Layer 1</title><path d="M209.247 329.98L52.368 387.638l121.325 85.822 96.752-95.804-61.198-47.674z" fill="#4db6ac" fill-rule="evenodd" stroke-width=".3"/><path d="M480.193 71.446c-13.123 1.784-9.565 1.013-28.4 16.09-18.008 14.418-69.925 100.347-97.673 129.256-24.688 25.722-34.46 12.199-60.102 33.661-25.68 21.494-65.273 64.464-65.273 64.464l63.978 47.32L394.15 222.754c23.948-32.932 23.694-37.266 36.744-71.82 6.384-16.907 17.76-29.9 27.756-45.809 12.488-19.874 30.186-34.855 21.543-33.68z" fill="#00897b" fill-rule="evenodd" stroke-width=".3"/><path d="M478.206 69.796c-31.268-.189-62.068 137.245-115.56 242.691-54.543 107.519-162.235 176.82-162.235 176.82 18.156 8.243 34.681 4.91 54.236 23.394 13.375 16.164 52.09 95.976 75.174 146.117 0 0 18.964-10.297 42.994-27.695 24.03-17.397 53.124-41.896 73.384-70.3 26.883-37.692 47.897-61.043 65.703-75.271 17.806-14.23 32.404-19.336 46.458-20.54 50.238-4.305 124.582 85.792 124.582 85.792S527.267 70.09 478.206 69.796z" fill="#ffb74d" fill-rule="evenodd" stroke-width=".3"/></symbol><symbol viewBox="0 0 24 24" id="merlin" xmlns="http://www.w3.org/2000/svg"><text style="line-height:1.25;-inkscape-font-specification:'Century Gothic Bold'" x="1.953" y="21.178" transform="scale(.99582 1.0042)" font-weight="700" font-size="30.255" font-family="Century Gothic" letter-spacing="0" word-spacing="0" fill="#42a5f5" stroke-width=".756"><tspan x="1.953" y="21.178" style="-inkscape-font-specification:'Century Gothic Bold'" font-size="22.745">M</tspan></text></symbol><symbol viewBox="0 0 192 191.99999" id="mocha" xmlns="http://www.w3.org/2000/svg"><title>Mocha Logo</title><g transform="translate(-354.75 -262.42) scale(4.835)" fill="#a1887f"><path d="M103.6 69.6c0-.5-.4-1-1-1H83.8c-.5 0-1 .4-1 1 0 3.4.5 15.1 5.5 20.8.2.2.4.3.7.3h8.4c.3 0 .5-.1.7-.3 5-5.6 5.5-17.3 5.5-20.8zm-7.4 18.2h-5.9c-.3 0-.5-.1-.7-.3-3.4-4-3.8-12-3.9-14.8 0-.5.4-1 1-1h13.2c.5 0 1 .4 1 1 0 2.8-.5 10.7-3.9 14.8-.3.2-.5.3-.8.3zM95.1 66.6s3.6-2.1 1.4-5.9c-1.3-2-1.9-3.7-1.4-4.4-1.3 1.6-3.5 3.3-1.1 6.9.8.9 1.2 2.8 1.1 3.4zM91.1 66.9s2.4-1.4.9-4c-.9-1.3-1.3-2.5-.9-2.9-.9 1.1-2.3 2.2-.7 4.7.5.5.7 1.8.7 2.2z"/><path d="M99.3 78.5c-.4 2.7-1.2 5.8-2.9 7.8-.2.2-.4.3-.6.3h-5c-.2 0-.5-.1-.6-.3-1.2-1.5-2-3.5-2.5-5.6 0 0 5.8.8 9.1-.4 2.4-.9 2.5-1.8 2.5-1.8z"/></g></symbol><symbol viewBox="0 0 24 24" id="movie" xmlns="http://www.w3.org/2000/svg"><path d="M18 4l2 4h-3l-2-4h-2l2 4h-3l-2-4H8l2 4H7L5 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V4h-4z" fill="#ff9800"/></symbol><symbol viewBox="0 0 24 24" id="music" xmlns="http://www.w3.org/2000/svg"><path d="M16 9V7h-4v5.5c-.42-.31-.93-.5-1.5-.5A2.5 2.5 0 0 0 8 14.5a2.5 2.5 0 0 0 2.5 2.5 2.5 2.5 0 0 0 2.5-2.5V9h3m-4-7a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2z" fill="#ef5350"/></symbol><symbol viewBox="0 0 24 24" id="mxml" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m.12 13.5l3.74 3.74 1.42-1.41-2.33-2.33 2.33-2.33-1.42-1.41-3.74 3.74m11.16 0l-3.74-3.74-1.42 1.41 2.33 2.33-2.33 2.33 1.42 1.41 3.74-3.74z" fill="#ffa726"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-actions" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#ab47bc" stroke-width="12.914"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-effects" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#26c6da" stroke-width="12.914"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-reducer" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#e53935" stroke-width="12.914"/></symbol><symbol viewBox="0 0 300 300" id="ngrx-state" xmlns="http://www.w3.org/2000/svg"><path d="M150 27.324L35.85 68.006l17.303 151.09 96.843 53.586 96.843-53.586 17.303-151.09zm-23.719 38.349c4.346-.075 9.04 1.316 14.265 4.131 2.3 1.24 9.235 2.994 15.407 3.889 21.936 3.18 47.975 19.934 56.21 36.186 5.667 11.183 4.508 17.209-4.18 21.702-7.492 3.874-22.822 2-45.08-5.517l-18.785-6.343-6.683 2.552c-9.683 3.698-19.366 12.877-23.33 22.09-2.858 6.645-3.293 9.768-2.77 20.705.523 10.955 1.315 14.12 5.2 20.997 4.423 7.829 14.576 17.818 16.331 16.064.473-.473-.574-3.648-2.308-7.048-1.735-3.4-2.744-6.825-2.26-7.606.482-.781 5.054 2.123 10.157 6.44 11.35 9.6 24.608 15.74 36.77 17.01 9.985 1.045 12.266-.814 4.787-3.912-2.41-.998-5.544-3.088-6.95-4.641-2.907-3.212-3.072-3.12 9.356-5.906 7.736-1.733 23.026-9.849 23.937-12.71.29-.91-2.195-1.296-6.27-.972-3.706.295-6.732-.087-6.732-.85 0-.76 3.032-4.523 6.732-8.385 13.883-14.489 18.62-25.32 20.098-45.906l1.02-14.217 3.257 6.756c3.601 7.452 4.265 18.202 1.701 27.437-2.141 7.711-.712 8.564 3.208 1.92 4.845-8.212 6.39-6.905 5.54 4.666-.924 12.587-5.243 22.017-14.993 32.686-7.95 8.699-7.001 10.254 2.624 4.326 9.273-5.711 10.511-4.815 5.736 4.155-9.031 16.964-28.122 31.35-47.948 36.161-12.016 2.917-20.537 3.461-31.544 2.018-28.78-3.775-56.001-23.157-68.993-49.114-3.378-6.748-8.154-14.994-10.62-18.348-5.092-6.924-5.529-10.038-2.09-15.286 1.715-2.618 2.116-5.307 1.41-9.308-3.273-18.531-3.167-19.11 4.276-26.659 6.468-6.56 6.878-7.44 6.878-15.092 0-6.637.671-8.813 3.67-11.811 2.02-2.02 5.23-3.7 7.12-3.718 5.49-.05 14.97-5.135 20.584-11.033 4.687-4.927 9.674-7.417 15.262-7.51z" fill="#9ccc65" stroke-width="12.914"/></symbol><symbol viewBox="0 0 24 24" id="nim" xmlns="http://www.w3.org/2000/svg"><path d="M4.464 15.75L2.288 3.78l5.985 7.617L12.08 3.78l3.809 7.617 5.985-7.617-2.177 11.97H4.464m15.234 3.264a1.088 1.088 0 0 1-1.088 1.088H5.553a1.088 1.088 0 0 1-1.089-1.088v-1.089h15.234z" stroke-width="1.088" fill="#ffca28"/></symbol><symbol viewBox="0 0 500 500" id="nix" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-1.965 36.302)" stroke-width=".395"><path d="M135.59 415.7c0-.295-2.752-5.283-6.116-11.084-3.364-5.801-6.116-10.776-6.116-11.055s9.514-16.889 21.143-36.912c11.629-20.022 21.323-36.798 21.542-37.279.346-.76-1.608-4.363-14.896-27.466-8.412-14.625-15.294-26.785-15.294-27.023 0-.5 24.46-43.501 25.206-44.31.414-.45.592-.384 1.078.395.32.513 16.876 29.256 36.791 63.87 62.62 108.85 74.852 130.01 75.41 130.46.3.242.544.554.544.694 0 .14-11.836.21-26.302.154-23.023-.09-26.313-.175-26.393-.694-.11-.714-27.662-48.825-28.86-50.392-.746-.978-.906-1.035-1.426-.51-.688.696-28.954 49.323-29.49 50.733l-.365.96h-13.229c-10.896 0-13.229-.095-13.229-.538zm167.58-125.61c-.134-.216 1.188-2.863 2.938-5.882 6.924-11.944 84.291-145.75 96.491-166.88 7.143-12.371 13.142-22.465 13.333-22.433.363.062 25.861 43.105 25.861 43.655 0 .174-6.761 11.952-15.026 26.173-8.46 14.557-14.932 26.104-14.81 26.421.185.483 4.564.564 30.213.564h29.996l.958 1.48c.526.814 3.296 5.547 6.155 10.518 2.859 4.971 5.45 9.29 5.756 9.597.706.705.704.724-.16 1.572-.395.388-3.36 5.323-6.587 10.965-3.228 5.643-6.056 10.387-6.285 10.543-.23.156-19.695.171-43.256.034l-42.84-.249-.804 1.15c-.441.632-7.504 12.736-15.696 26.897l-14.892 25.747H339.03c-8.517 0-20.015.116-25.55.259-6.55.168-10.15.121-10.309-.135zM169.42 132.23c-56.373-.055-102.5-.182-102.5-.282 0-.1 5.617-10.132 12.481-22.294l12.481-22.112h30.332c27.113 0 30.332-.065 30.332-.611 0-.336-6.659-12.228-14.797-26.427-8.139-14.199-14.797-25.917-14.797-26.04 0-.123 2.682-4.853 5.96-10.51s6.003-10.578 6.055-10.934c.086-.586 1.376-.648 13.572-.648 7.413 0 13.463.143 13.446.317-.017.174.222.707.531 1.184.31.476 9.763 16.937 21.007 36.578 11.244 19.64 20.71 36.022 21.036 36.4.554.647 2.549.691 31.428.691h30.837l12.896 22.145c7.093 12.18 12.8 22.301 12.682 22.492-.118.19-4.776.303-10.352.249-5.575-.054-56.26-.143-112.63-.198z" fill="#5075c1"/><path d="M25.289 203.14c-6.098 10.563-6.69 11.711-6.225 12.078.283.224 3.18 5.044 6.44 10.712 3.261 5.668 6.017 10.355 6.124 10.417.106.061 13.585.153 29.95.204 16.367.052 29.994.23 30.285.399.472.273-1.08 3.094-14.637 26.574L62.06 289.793l12.907 21.865c7.1 12.026 12.982 21.906 13.068 21.956.086.05 23.257-39.831 51.492-88.624 11.352-19.617 21.214-36.64 30.37-52.442 23.308-40.452 30.68-53.468 30.73-54.132-1.097-.11-6.141-.187-13.006-.216-3.945-.01-7.82-.02-12.75-.002l-25.341.092-15.42 26.706c-14.256 24.693-15.445 26.663-16.278 26.86l-.024.037c-.011.003-1.62-.001-1.825 0-4.29.062-20.453.063-40.226-.01-22.632-.082-41.615-.125-42.183-.096-.568.03-1.147-.03-1.29-.132-.142-.102-3.29 5.066-6.996 11.485zm205.16-190.3c-.123.149 5.62 10.392 12.761 22.763 12.199 21.131 89.393 155.03 96.276 167 1.502 2.613 2.92 4.803 3.443 5.348.9-1.249 3.531-5.63 7.954-13.219a1342.88 1342.88 0 0 1 10.049-17.76l6.606-11.443c.692-1.403.754-1.818.653-2.117-.162-.48-6.904-12.332-14.982-26.337-8.078-14.005-14.824-25.849-14.991-26.32a.73.73 0 0 1-.009-.366l-.426-.913L359.42 72.5c3.69-6.307 6.425-11.042 9.47-16.29 9.159-15.948 12.037-21.189 11.896-21.55-.126-.324-2.7-4.83-5.72-10.017-3.021-5.185-5.845-10.148-6.275-11.026-.483-.987-.734-1.364-1.1-1.456-.054.014-.083.018-.145.035-.42.112-5.454.195-11.189.185-5.734-.01-11.22.024-12.188.073l-1.76.089-14.997 25.978c-12.824 22.212-15.084 25.964-15.595 25.883-.024-.004-.15-.189-.235-.301-.109.066-.2.09-.272.05-.255-.148-7.143-11.902-15.306-26.119l-14.36-25.016c-.115-.186-.444-.744-.457-.752-.477-.275-50.502.287-50.737.57zm-18.646 283.09c-.047.109-.026.262.042.48.329 1.05 25.338 43.735 25.772 43.985.207.119 14.178.239 31.05.266 26.651.044 30.75.152 31.234.832.308.43 9.988 17.214 21.513 37.296s21.152 36.627 21.394 36.767c.242.14 5.927.243 12.633.23 6.706-.013 12.401.099 12.657.246.132.076.382-.141.852-.795l6.008-10.406c5.234-9.065 6.62-11.684 6.294-11.888-.575-.36-15.597-26.643-23.859-41.482-3.09-5.45-5.37-9.516-5.441-9.774-.195-.712-.065-.822 1.156-.98 1.956-.252 57.397-.057 58.07.205.238.092.79-.569 2.594-3.497 1.866-3.067 5.03-8.524 11-18.866 7.22-12.505 13.044-22.784 12.942-22.843-.102-.059-.771-.051-1.489.016l-.046.001c-4.452.204-33.918.203-149.74.025-38.96-.06-69.786-.09-71.912-.072-1.121.01-2.095.076-2.66.172a.25.25 0 0 0-.062.083z" fill="#7db7e1"/></g></symbol><symbol viewBox="0 0 24 24" id="nodejs" xmlns="http://www.w3.org/2000/svg"><path d="M12 1.85c-.27 0-.55.07-.78.2l-7.44 4.3c-.48.28-.78.8-.78 1.36v8.58c0 .56.3 1.08.78 1.36l1.95 1.12c.95.46 1.27.47 1.71.47 1.4 0 2.21-.85 2.21-2.33V8.44c0-.12-.1-.22-.22-.22H8.5c-.13 0-.23.1-.23.22v8.47c0 .66-.68 1.31-1.77.76L4.45 16.5a.26.26 0 0 1-.11-.21V7.71c0-.09.04-.17.11-.21l7.44-4.29c.06-.04.16-.04.22 0l7.44 4.29c.07.04.11.12.11.21v8.58c0 .08-.04.16-.11.21l-7.44 4.29c-.06.04-.16.04-.23 0L10 19.65c-.08-.03-.16-.04-.21-.01-.53.3-.63.36-1.12.51-.12.04-.31.11.07.32l2.48 1.47c.24.14.5.21.78.21s.54-.07.78-.21l7.44-4.29c.48-.28.78-.8.78-1.36V7.71c0-.56-.3-1.08-.78-1.36l-7.44-4.3c-.23-.13-.5-.2-.78-.2M14 8c-2.12 0-3.39.89-3.39 2.39 0 1.61 1.26 2.08 3.3 2.28 2.43.24 2.62.6 2.62 1.08 0 .83-.67 1.18-2.23 1.18-1.98 0-2.4-.49-2.55-1.47a.226.226 0 0 0-.22-.18h-.96c-.12 0-.21.09-.21.22 0 1.24.68 2.74 3.94 2.74 2.35 0 3.7-.93 3.7-2.55 0-1.61-1.08-2.03-3.37-2.34-2.31-.3-2.54-.46-2.54-1 0-.45.2-1.05 1.91-1.05 1.5 0 2.09.33 2.32 1.36.02.1.11.17.21.17h.97c.05 0 .11-.02.15-.07.04-.04.07-.1.05-.16C17.56 8.82 16.38 8 14 8z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 300 300" id="nodemon" xmlns="http://www.w3.org/2000/svg"><title>nodemon</title><path d="M149.868 20.62c-2.124 0-4.25.55-6.154 1.648L41.899 81.083a12.306 12.306 0 0 0-6.15 10.652v117.633a12.29 12.29 0 0 0 6.152 10.646l101.815 58.766h.001a12.282 12.282 0 0 0 12.291 0l101.84-58.766a12.29 12.29 0 0 0 6.153-10.652V91.738a12.31 12.31 0 0 0-6.146-10.652L156.015 22.27a12.302 12.302 0 0 0-6.153-1.648zM83.303 70.93s11.789 33.031 35.477 31.934l27.74-15.961a7.348 7.348 0 0 1 3.414-.99h.641a7.233 7.233 0 0 1 3.404.99l27.738 15.961c23.69 1.094 35.475-31.934 35.475-31.934 5.233 23.154 1.06 38.641-5.924 48.942l4.541 2.614h.002c2.321 1.327 3.734 3.795 3.737 6.49l-.12 95.811a3.724 3.724 0 0 1-1.855 3.227 3.624 3.624 0 0 1-3.735 0L177.1 206.971c-2.311-1.363-3.742-3.818-3.742-6.48v-44.763a7.44 7.44 0 0 0-3.737-6.465l-15.642-9.01a7.28 7.28 0 0 0-3.715-1.01 7.378 7.378 0 0 0-3.742 1.01l-15.648 9.01c-2.316 1.323-3.729 3.798-3.729 6.467v44.762c0 2.663-1.413 5.1-3.738 6.48l-36.748 21.041a3.571 3.571 0 0 1-3.71 0c-1.173-.65-1.864-1.887-1.864-3.224l-.137-95.812a7.483 7.483 0 0 1 3.74-6.49l4.541-2.615c-6.982-10.302-11.16-25.79-5.925-48.942z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 990 990" id="npm" xmlns="http://www.w3.org/2000/svg"><defs><style>.hncls-1{fill:#cb3837}.cls-2{fill:#fff}</style></defs><title>n</title><path class="hncls-1" d="M113.26 876.74V113.27h763.47v763.47zm143.59-620.4v476.18h240.61V355.63h140.21v376.96h95.457V256.34z" fill="#e53935" stroke-width=".771"/></symbol><symbol id="nunjucks" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.host0{fill:#388e3c}</style><path class="host0" d="M11.2 21.1H8.1l-2.3-7.9v7.9H2.7V2.9h3.1l2.3 7.4V2.9h3.1zM21.3 19.2c0 1-.8 1.9-1.9 1.9h-4.8c-1 0-1.9-.8-1.9-1.9v-3.8l3.2-.7V18h2.3V7.2h3.1v12z"/></symbol><symbol viewBox="0 0 150 150.00001" id="ocaml" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.76136 0 0 .76136 11.616 19.98)"><path d="M83.02 101.645l.023-.062c-.035-.159-.047-.195-.024.062z" fill="none" stroke-width="1.028"/><linearGradient id="hpa" gradientUnits="userSpaceOnUse" x1="-696.735" y1="97.7" x2="-696.735" y2="142.997" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><path d="M82.313 138.79c-.471-1.004-1.904-3.621-2.624-4.46-1.562-1.828-1.927-1.966-2.386-4.275-.799-4.02-2.913-11.31-5.405-16.341-1.286-2.596-3.426-4.777-5.385-6.66-1.71-1.652-5.565-4.431-6.237-4.294-6.296 1.257-8.249 7.432-11.21 12.323-1.638 2.705-3.374 5.007-4.665 7.885-1.192 2.646-1.087 5.577-3.128 7.849-2.093 2.333-3.454 4.814-4.48 7.829-.194.574-.747 6.596-1.348 8.015l9.357-.659c8.719.594 6.2 3.936 19.81 3.208l21.487-.665c-.666-1.97-1.584-4.25-1.938-4.991-.599-1.248-1.352-3.69-1.848-4.763z" fill="url(#hpa)" stroke-width="1.028"/><linearGradient id="hpb" gradientUnits="userSpaceOnUse" x1="-666.972" y1="142.12" x2="-666.972" y2="142.12" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><linearGradient id="hpc" gradientUnits="userSpaceOnUse" x1="-675.228" y1="-1.28" x2="-675.228" y2="142.967" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><path d="M109.553 94.296c-1.652 1.193-4.88 4.06-11.902 5.145-3.152.487-6.1.527-9.335.365-1.584-.076-3.077-.157-4.665-.177-.936-.008-4.074-.107-3.919.193l-.349.871c.054.287.169 1.004.2 1.177.129.704.165 1.265.192 1.912.048 1.331-.11 2.719-.043 4.062.141 2.787 1.175 5.326 1.306 8.137.143 3.13 1.69 6.442 3.188 8.998.569.973 1.434 1.084 1.811 2.283.442 1.373.024 2.83.239 4.293.842 5.675 2.477 11.606 5.032 16.728.018.043.038.09.06.128 3.156-.53 6.318-1.665 10.418-2.271 7.517-1.115 17.972-.54 24.688-1.17 16.993-1.597 26.216 6.97 41.478 3.459V22.459c0-11.84-9.594-21.438-21.435-21.438H19.239C7.4 1.021-2.197 10.62-2.197 22.458v46.774c3.067-1.11 7.479-7.635 8.861-9.222 2.419-2.775 2.858-6.315 4.062-8.544 2.743-5.078 3.215-8.57 9.451-8.57 2.907 0 4.061.67 6.027 3.31 1.368 1.834 3.731 5.224 4.837 7.49 1.277 2.615 3.357 6.153 4.272 6.867.677.53 1.35.928 1.976 1.163 1.012.38 1.848-.316 2.525-.855.863-.687 1.235-2.088 2.035-3.957 1.152-2.696 2.408-5.926 3.122-7.054 1.237-1.949 1.658-4.261 2.993-5.381 1.97-1.652 4.54-1.768 5.246-1.908 3.957-.781 5.755 1.906 7.704 3.645 1.276 1.138 3.019 3.432 4.256 6.507.967 2.4 2.199 4.622 2.714 6.008.497 1.339 1.725 3.484 2.453 6.055.661 2.336 2.43 4.125 3.102 5.235 0 0 1.029 2.882 7.285 5.516 1.357.572 4.1 1.501 5.736 2.096 2.718.988 5.351.86 8.704.458 2.391 0 3.686-3.462 4.772-6.234.643-1.639 1.259-6.334 1.678-7.667.406-1.297-.544-2.3.265-3.437.946-1.327 1.508-1.399 2.054-3.129 1.172-3.704 7.95-3.89 11.761-3.89 3.176 0 2.772 3.083 8.16 2.028 3.086-.605 6.059.398 9.335 1.265 2.758.732 5.352 1.566 6.906 3.385 1.005 1.178 3.5 7.08.958 7.331.244.3.423.84.88 1.135-.566 2.226-3.03.64-4.4.355-1.845-.383-3.147.057-4.952.856-3.085 1.374-7.598 1.214-10.286 3.452-2.281 1.898-2.277 6.133-3.34 8.507-.002-.001-2.955 7.6-9.402 12.248z" fill="url(#hpc)" stroke-width="1.028"/><linearGradient id="hpd" gradientUnits="userSpaceOnUse" x1="-735.137" y1="90.833" x2="-735.137" y2="141.967" gradientTransform="matrix(1.02783 0 0 1.02783 776.895 2.337)"><stop offset="0" stop-color="#f29100"/><stop offset="1" stop-color="#ec670f"/></linearGradient><path d="M38.247 105.09c-1.467-.15-2.83-.317-4.256-.605-2.662-.536-5.57-1.06-8.193-1.688-1.592-.385-6.895-2.263-8.048-2.792-2.702-1.246-4.496-4.63-6.609-4.282-1.348.22-2.662.682-3.5 2.042-.685 1.11-.917 3.016-1.391 4.294-.55 1.485-1.5 2.87-2.331 4.284-1.53 2.595-4.282 4.941-5.468 7.469-.239.52-.45 1.101-.649 1.708V144.415a48.57 48.57 0 0 1 4.45.96c11.955 3.19 14.872 3.46 26.598 2.119l1.1-.146c.897-1.867 1.59-8.227 2.171-10.195.454-1.51 1.077-2.712 1.313-4.253.223-1.463-.02-2.858-.146-4.188-.329-3.332 2.427-4.522 3.742-7.384 1.186-2.589 1.871-5.535 2.853-8.181.941-2.54 2.41-6.13 4.918-7.408-.305-.355-5.237-.518-6.554-.65z" fill="url(#hpd)" stroke-width="1.028"/></g></symbol><symbol viewBox="0 0 24 24" id="pdf" xmlns="http://www.w3.org/2000/svg"><path d="M14 9h5.5L14 3.5V9M7 2h8l6 6v12a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m4.93 10.44c.41.9.93 1.64 1.53 2.15l.41.32c-.87.16-2.07.44-3.34.93l-.11.04.5-1.04c.45-.87.78-1.66 1.01-2.4m6.48 3.81c.18-.18.27-.41.28-.66.03-.2-.02-.39-.12-.55-.29-.47-1.04-.69-2.28-.69l-1.29.07-.87-.58c-.63-.52-1.2-1.43-1.6-2.56l.04-.14c.33-1.33.64-2.94-.02-3.6a.853.853 0 0 0-.61-.24h-.24c-.37 0-.7.39-.79.77-.37 1.33-.15 2.06.22 3.27v.01c-.25.88-.57 1.9-1.08 2.93l-.96 1.8-.89.49c-1.2.75-1.77 1.59-1.88 2.12-.04.19-.02.36.05.54l.03.05.48.31.44.11c.81 0 1.73-.95 2.97-3.07l.18-.07c1.03-.33 2.31-.56 4.03-.75 1.03.51 2.24.74 3 .74.44 0 .74-.11.91-.3m-.41-.71l.09.11c-.01.1-.04.11-.09.13h-.04l-.19.02c-.46 0-1.17-.19-1.9-.51.09-.1.13-.1.23-.1 1.4 0 1.8.25 1.9.35M8.83 17c-.65 1.19-1.24 1.85-1.69 2 .05-.38.5-1.04 1.21-1.69l.48-.31m3.02-6.91c-.23-.9-.24-1.63-.07-2.05l.07-.12.15.05c.17.24.19.56.09 1.1l-.03.16-.16.82-.05.04z" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="perl" xmlns="http://www.w3.org/2000/svg"><path d="M12 14c-1 0-3 1-3 2 0 2 3 2 3 2v-1a1 1 0 0 1-1-1 1 1 0 0 1 1-1v-1m0 5s-4-.5-4-2.5c0-3 3-3.75 4-3.75V11.5c-1 0-5 1.5-5 4.5 0 4 5 4 5 4v-1M10.07 7.03l1.19.53c.43-2.44 1.58-4.06 1.58-4.06-.43 1.03-.71 1.88-.89 2.55C13.16 3.55 15.61 2 15.61 2a15.916 15.916 0 0 0-2.64 3.53c1.58-1.68 3.77-2.78 3.77-2.78-2.69 1.72-3.9 4.45-4.2 5.21l.55.08c0 .52 0 1 .25 1.38C14.1 11.31 18 11.47 18 16s-4.03 6-6.17 6C9.69 22 5 21.03 5 16s4.95-5.07 5.83-7.08c.12-.38-.76-1.89-.76-1.89z" fill="#9575cd"/></symbol><symbol viewBox="0 0 24 24" id="php" xmlns="http://www.w3.org/2000/svg"><path d="M12 18.08c-6.63 0-12-2.72-12-6.08s5.37-6.08 12-6.08S24 8.64 24 12s-5.37 6.08-12 6.08m-5.19-7.95c.54 0 .91.1 1.09.31.18.2.22.56.13 1.03-.1.53-.29.87-.58 1.09-.28.22-.71.33-1.29.33h-.87l.53-2.76h.99m-3.5 5.55h1.44l.34-1.75h1.23c.54 0 .98-.06 1.33-.17.35-.12.67-.31.96-.58.24-.22.43-.46.58-.73.15-.26.26-.56.31-.88.16-.78.05-1.39-.33-1.82-.39-.44-.99-.65-1.82-.65H4.59l-1.28 6.58m7.25-8.33l-1.28 6.58h1.42l.74-3.77h1.14c.36 0 .6.06.71.18.11.12.13.34.07.66l-.57 2.93h1.45l.59-3.07c.13-.62.03-1.07-.27-1.36-.3-.27-.85-.4-1.65-.4h-1.27L12 7.35h-1.44M18 10.13c.55 0 .91.1 1.09.31.18.2.22.56.13 1.03-.1.53-.29.87-.57 1.09-.29.22-.72.33-1.3.33h-.85l.5-2.76h1m-3.5 5.55h1.44l.34-1.75h1.22c.55 0 1-.06 1.35-.17.35-.12.65-.31.95-.58.24-.22.44-.46.58-.73.15-.26.26-.56.32-.88.15-.78.04-1.39-.34-1.82-.36-.44-.99-.65-1.82-.65h-2.75l-1.29 6.58z" fill="#1E88E5"/></symbol><symbol viewBox="0 0 79 78" id="postcss" xmlns="http://www.w3.org/2000/svg"><title>postcss-logo-symbol</title><g transform="translate(5.48 5.52) scale(.85425)" fill="#e53935" fill-rule="evenodd" stroke="#e53935" stroke-width="1.519"><path d="M15.447 32.623c.106.08.29.132.106.29-.132.184-.29.342-.395.553-.105.185-.184.237-.342.106.21-.343.42-.66.63-.95zM68.342 60.24c0 .078.026.13.026.21.053-.105.053-.158.08-.21zm0 .236v-.026zm-5.368 10.277l-4.58-25.402c-.078-.025-.183-.077-.368-.13.053.105.08.184.106.263.13-.026.184-.026.236-.052 0-.026 0-.052.027-.08l4.58 25.404zm-4.737-31.12c-.026.078-.026.158-.026.237 0-.08 0-.16.028-.238zm.026.526c-.026 0-.026 0-.052-.028v.026c.028.026.028.026.054 0zm-.052.21v-.185c-.077.026-.156.026-.262.053.132.05.264.078.264.13z"/><path d="M78.71 33.967c-.052-1.028-.078-2.056-.184-3.083-.184-1.397-.368-2.82-.684-4.19-.237-1.133-.63-2.214-1.026-3.294-.5-1.265-1-2.556-1.632-3.768-1.026-1.95-2.368-3.69-3.605-5.508-.818-1.16-1.87-2.108-2.66-3.294-.447-.685-1.105-1.264-1.763-1.79-1.053-.845-2.158-1.61-3.263-2.347a32.525 32.525 0 0 0-2.58-1.634c-.71-.397-1.473-.713-2.21-1.056-.842-.395-1.658-.87-2.605-1.054-.238-.05-.448-.13-.685-.21-.605-.21-1.184-.447-1.79-.632-.92-.29-1.815-.632-2.763-.87C50.342 1 49.394.843 48.446.71 47.394.555 46.316.5 45.262.397a26.83 26.83 0 0 0-2.026-.184C42.236.16 41.21.16 40.21.134c-.5-.027-1.026-.08-1.526-.053-.763.026-1.526.105-2.29.21-.736.08-1.473.21-2.183.317-.867.105-1.735.158-2.604.264-.816.106-1.658.264-2.473.396-.29.053-.58.158-.87.21-.63.132-1.288.185-1.92.396-1.13.344-2.263.74-3.368 1.16-1.027.422-2.027.87-3 1.397-1 .552-1.948 1.21-2.895 1.844a45.325 45.325 0 0 0-2.66 1.923c-.84.66-1.63 1.397-2.394 2.135-.42.42-.763.922-1.158 1.396-.657.765-1.315 1.502-1.947 2.293-.524.66-1 1.344-1.5 2.03-.893 1.21-1.656 2.502-2.366 3.794-.29.527-.553 1.054-.816 1.58-.395.79-.816 1.555-1.184 2.372-.264.554-.474 1.16-.632 1.766-.367 1.292-.736 2.61-1.078 3.9-.316 1.16-.395 2.372-.42 3.558-.027 1.054.078 2.082.183 3.136.027.264-.13.58.184.79-.105.29-.026.45.13.5-.182.29.08.476-.024.74-.027.052.08.157.13.236 0 .08-.025.185 0 .264.028.237.133.474.133.738 0 .184.157.395.21.58.026.078 0 .21-.053.263-.158.184-.132.342.105.448.133.342.08.5.054.66.052.236-.027.315 0 .368.21.422.29.896.315 1.37 0 .106.053.212.106.343.026 0 0 .5 0 .5.13-.078.237-.104.368-.157.08.342.158.66.263.95.132.21.132.314.08.34.105.474.157.922.34 1.37 0-.5-.05-1-.13-1.475.368.132.684.263.895.263.027-.08.053-.184.08-.237-.158-.157-.29-.394-.448-.552.053.21 0 .29 0 .37-.105-.054-.237-.107-.368-.16.105-.13.21-.263.368-.42 0-.238-.13-.45-.5-.423.158-.052.316-.13.5-.184.29-.157-.026-.447-.026-.816.026-.447-.237-.895-.316-1.37-.132-.737-.105-1.844-.184-2.582-.158-.132-.29.21-.316.237.08.632.158 1.264.21 1.897-.157-.527-.263-1.107-.394-1.74-.027.185-.053.264-.053.37-.13.13-.026.29.053.474-.184-.08-.395-.052-.395-.052v.738c-.262-.264-.34-.474-.473-.66-.052-.21-.08-.42-.13-.63.05-.133 0-.212 0-.29a15.968 15.968 0 0 1-.08-.634c.026-.026-.026-.42-.026-.42.21.025.343.05.474.05-.263-.34-.08-.552.027-.763.053-.106.237-.13.29-.238.21-.395.553-.71.553-1.212 0-.237.08-.5.105-.738.053-.448.105-.896.13-1.344.054-.58 0-1.16.133-1.713.212-.92.475-1.843.764-2.766.21-.66.448-1.29.71-1.95.395-1.028.764-2.056 1.264-3.03.71-1.424 1.526-2.794 2.316-4.19.5-.87 1.026-1.687 1.58-2.53.525-.817 1.05-1.66 1.657-2.425a21.452 21.452 0 0 1 2.79-2.978c1.053-.948 2.053-1.923 3.184-2.793a32.218 32.218 0 0 1 4.685-3.005c1.343-.71 2.737-1.266 4.132-1.793.895-.342 1.868-.5 2.79-.79 1.052-.343 2.105-.5 3.21-.527.71-.027 1.395-.106 2.105-.185.632-.05 1.263-.104 1.948-.183-.08.105-.106.158-.132.21-.288.422-.604.844-.894 1.265-.237.343-.5.712-.737 1.054-.422.555-.87 1.108-1.264 1.688-.605.87-1.158 1.766-1.79 2.635-.63.843-1.315 1.634-1.973 2.45-.868 1.134-1.684 2.293-2.552 3.426-.79 1.08-1.63 2.11-2.394 3.19-.684.947-1.29 1.95-1.948 2.923-.973 1.45-1.947 2.872-2.92 4.322a271.93 271.93 0 0 1-2.316 3.294c-.053.08-.132.104-.21.157-.21.342-.21.527-.29.685-.21.395-.42.79-.658 1.16-.132.21-.316.394-.474.605-.026-.316.42-.474.21-.87-.13.212-.263.396-.394.607l-.316.63c.105.08.29.133.105.29-.08.133-.158.29-.237.423a.954.954 0 0 0 .29-.264c0 .29-.158.526-.29.763-.105.21-.368.37-.552.527.026.027.21.106.237.132.237-.08.316-.21.343-.132.08-.105.158-.184.184-.263.104-.264.262-.474.525-.58.106-.053.184-.132.263-.21.79-.818 1.606-1.608 2.316-2.478 1.106-1.345 2.106-2.74 3.16-4.11.446-.58.973-1.16 1.446-1.714.078.606.026 1.185 0 1.74-.08.974-.132 1.95-.21 2.95-.027.395 0 .79-.027 1.186 0 .105-.08.184-.08.29 0 .263.08.553.08.817-.08.975-.186 1.923-.265 2.898-.027.21.078.422.13.607-.13 1.422.16 2.925-.078 4.427.184-.29.237-.474.237-.658.025-.158 0-.316 0-.5v-.264c.025-.475.13-.975.078-1.45-.053-.527-.053-1.027.053-1.528.053-.21-.026-.474.106-.738v.395c-.026 1.5.027 3.003-.183 4.505-.027.132.08.37-.21.343-.238.474.052.817-.21 1.08-.054.053.05.29.077.448-.106.317-.106.317.052.343.026.58.08 1.106.105 1.66.42-1 .21-2.03.396-3.058.026.422.053.844.026 1.29 0 .687-.026 1.345-.052 2.03 0 .132-.027.264-.053.396-.08.37-.105.738-.237 1.08-.105.264-.052.66-.052.975v1.003c.105.448-.027.685.052.948-.08.265-.105.344-.08.423l.08.395c.527-.053.29.343.5.553-.158.212-.105.29-.105.397 0 .237-.025.448-.052.685 0 .606-.026 1.212-.026 1.792 0 .08.026.157.026.236 0 .054-.026.74-.026.74.053.078 0 .157-.08.236-.025 0-.104-3.347-.104-3.347h-.395c-.052 1.58.08 3.003-.21 4.48-.316.025-.42.078-.764.078-.816 0-1.632 0-2.448.026-.974 0-1.92.026-2.895.026-.472 0-.972.054-1.446.054-.632 0-1.29-.08-1.92-.08-.975 0-1.922.08-2.896.106-.71.026-1.42.026-2.13.053-.475.025-.95.05-1.422.104-.21.026-.395.105-.658.184-.08 0-.263-.026-.42 0-.265.053-.5.21-.765.264-.395.08-.5.184-.448.58v.263c-.026.052.58-.08.58-.08-.054 0-.08.158-.16.29.212-.08.343-.132.475-.184.395.185.737.08 1.052.16 1.026.262 2.078.37 3.13.473.685.053 1.343.08 2.027.105.973.053 1.947.106 2.92.106.816 0 1.606-.08 2.42-.08 1.13 0 2.264.052 3.395.08.237 0 .5-.028.763-.028h1.92c1.712-.052 3.422-.08 5.133-.13.975-.028 1.975-.08 2.948-.107l3-.08c1.158-.026 2.316-.026 3.448-.05.868 0 1.71-.03 2.58-.055.972-.026 1.972-.105 2.946-.157.527-.027 1.054-.08 1.58-.132.632-.052 1.29-.13 1.92-.157.948-.054 1.922-.08 2.87-.133 1.184-.078 2.368-.183 3.578-.21 1.106-.052 2.237-.026 3.343-.052.974-.027 1.948-.08 2.948-.106l1.66-.08s1.104-.026 1.657-.08c.947-.052 1.894-.157 2.842-.183.604-.027 1.21 0 1.815-.027.973-.026 1.973-.08 2.947-.08.367 0 .762.054 1.236.08-.21.185-.342.29-.5.422.105.026.21.08.316.132a.71.71 0 0 1-.42.13c-.054.133-.107.186-.16.45h.474c-.184 0-.342.237-.526.395-.21-.054-.395 0-.5.29.184.104.158.183.132.29-.316.104-.553.21-.42.552-.107.052-.238.105-.37.184-.13.21-.368.263-.316.553.106.025.21.08.29.104-.132.053-.263.132-.395.184-.473.29-.262.422-.157.554-.08.053-.158.105-.237.132.052.237.13.29.157.29a9.3 9.3 0 0 0-.395.316c-.08.237-.185.342-.29.5s-.158.37-.29.527c-.552.607-.947 1.32-1.657 1.793-.264.185-.5.422-.737.66-.474.447-.895.948-1.395 1.37a29.595 29.595 0 0 1-2.052 1.554 151.56 151.56 0 0 1-2.604 1.792c-.474.315-1 .552-1.5.842s-.974.554-1.474.843c-.316.21-.606.5-.948.66-.868.37-1.79.685-2.684 1.028-.87.37-1.5.685-2.158.922-.605.21-1.237.37-1.868.5-.21.054-.448 0-.685.027-.448.08-.895.186-1.343.238-1.158.158-2.316.264-3.473.422-.685.08-1.343.21-2.027.29-.473.026-.973-.026-1.447-.026-.342 0-.71.08-1.053.027-.552-.08-1.105-.21-1.658-.316-.13-.026-.316-.08-.42-.026-.21.106-.396-.052-.607 0-.13.027-.262-.08-.394-.08-.106-.025-.238.028-.37 0-.29-.078-.552-.183-.87-.157-.313.026-.63-.132-.97-.21-.475-.106-.92-.21-1.396-.317a2.38 2.38 0 0 1-.525-.237c-.685 0-1.133-.026-1.554-.185-.368-.13-.71-.315-1.105-.262-.104.026-.183-.026-.29-.026-.08-.106-.157-.317-.235-.317-.526.027-.842-.42-1.29-.553-.236-.08-.42-.343-.657-.422-.58-.237-1.052-.737-1.71-.816-.21-.027-.42-.132-.658-.21.08.104.13.183.21.262-.763-.37-1.473-.79-2.184-1.186-.104-.026-.183-.13-.262-.184l-.71-.474c-.395.08-.553-.08-.66-.132-.71-.5-1.525-.817-2.21-1.37-.29-.238-.63-.396-.84-.686-.37-.448-.817-.764-1.317-1.027-.394-.21-.762-.448-1.13-.685-.185-.132-.37-.29-.37-.58 0-.185-.078-.37-.315-.264-.105-.158-.21-.342-.342-.395-.316-.13-.526-.37-.763-.58s-.42-.5-.71-.605c-.527-.21-.843-.658-1.158-1.027-.738-.87-1.396-1.82-2.08-2.74-.053-.08-.158-.133-.237-.212.105.29.237.527.368.79-.262-.105-.446-.29-.604-.474-.027.027 1.815 3.057 1.815 3.057.16.237.29.475.448.712a.813.813 0 0 1-.79-.422c-.236-.42-.5-.684-1.026-.63a4.588 4.588 0 0 1-.13-.58c-.107 0-.185 0-.37-.027.37.58.685 1.08 1.027 1.66-.133-.08-.21-.132-.265-.158.473.5.815 1.133 1.42 1.45.132.605.816.895.974 1.475-.13-.027-.238-.053-.37-.08-.21-.263-.447-.526-.683-.816.052.184.13.342.236.474.316.395.606.79.974 1.133.132.134.316.187.316.424.21.105.29.13.368.13.054.16-.025.397.29.344.21.395.42.395.71.264.343.343.528.37.764.16 0 .13.026.262.026.368.105-.053.08-.132.08-.264.13.105.21.158.262.21.263.37.5.712.868 1.002.5.422.948.87 1.42 1.265.922.765 1.95 1.398 2.975 1.977 1.264.712 2.475 1.476 3.764 2.16 1.552.818 3.21 1.372 4.92 1.767.632.132 1.237.263 1.87.42.55.16 1.104.397 1.657.528.842.185 1.71.343 2.552.5.183.027.37.054.58.08.235.053.524-.053.577.027.132.21.237.104.395.078.184-.053.395-.053.605-.053.737.026 1.447.184 2.184.132.16 0 .396-.133.528.13.236-.105.368-.105.473-.13.028.236 0 .236-.05.262-.054.026-.133.053-.238.132.947.184 1.842.21 2.63 0 1.37.105 2.554-.053 3.686-.448.105.132.184.316.342.053.052-.08.184-.107.29-.133.236-.053.526-.158.736-.08.238.08.317-.13.5-.13.317 0 .606-.027.896-.08.158-.026.316-.105.5-.158a1.285 1.285 0 0 0-.58-.133c.317-.158.606-.29.896-.42-.053.078-.106.183-.21.183h.367c-.08 0-.185.237-.316.395.946-.237 1.814-.448 2.657-.66-.29-.552.315-.367.526-.684-.263.08-.526.158-.79.21.895-.447 1.816-.842 2.71-1.237-.13.158-.29.237-.525.37.158.025.263.025.342.05.42.133.316-.262.447-.5.5 0 .71-.078.947-.158.263-.08.526-.158.79-.263.42-.184.815-.42 1.236-.63.08-.028.21 0 .316 0 .29-.186.394-.344.473-.318.37.053.63-.08.736-.42.184-.133.316-.238.447-.318.578-.316 1.13-.632 1.71-.948.21 0 .316 0 .368-.027.344-.16.66-.342.975-.527a2.258 2.258 0 0 1-.263-.13c.262-.054.34-.08.5-.133.63-.74 1.5-1.24 2.157-1.82.29-.026.29-.105.29-.157.104-.132.21-.29.34-.396.58-.527 1.21-.975 1.737-1.528a37.16 37.16 0 0 0 2.184-2.374c.63-.738 1.264-1.475 1.79-2.292.737-1.133 1.368-2.293 2.026-3.48.474-.842.895-1.685 1.37-2.528.05-.08.157-.185.236-.185.71-.08 1.422-.13 2.106-.21.158-.026.342-.13.5-.21-.08-.132-.132-.29-.21-.422-.106-.16-.264-.29-.37-.45-.104-.13-.183-.29-.262-.447-.08-.13-.158-.236-.237-.37a9.7 9.7 0 0 1-.45-.894c-.026-.08-.08-.21-.052-.29.474-1.027.658-2.134 1.105-3.162.447-1.054.58-2.24.79-3.373.184-1.08.29-2.16.42-3.24.08-.764.185-1.502.21-2.266.16-1.212.106-2.346.08-3.48-.026-1-.08-2.028-.13-3.03zM12.685 66.405c-.184-.21-.342-.448-.526-.658l.08-.08c.287.317.577.633.866.976-.158-.08-.342-.132-.42-.238zm.42.238c.08-.027.16-.027.238-.053.08.132.132.29.21.448-.368-.027-.552-.185-.447-.395zm27.37 10.883v-.08c.5-.052.973-.105 1.473-.157v.077c-.5.08-.973.13-1.473.158zm6.63-.685c-.367.08-.762.133-1.13.186-.132.026-.29.158-.342-.08-.053.027-.106.027-.158.054.13.394.447.078.71.236-.58.08-1.13.132-1.684.21v-.052c.16-.026.343-.053.5-.08v-.078a7.743 7.743 0 0 0-.79-.053c-.077 0-.183.106-.262.132-.105.026-.21.053-.342.053-.447.026-.894.026-1.316.052-.027 0-.08-.026-.106-.026v-.08c1.763-.236 3.5-.473 5.263-.71.027.052.027.105.053.157-.158 0-.263.055-.395.08zm.396-.262c.606-.08 1.16-.132 1.738-.21-1.21.342-1.605.394-1.737.21zM24.58 23.374c.84-1.16 1.71-2.32 2.552-3.505.263-.345.473-.714.736-1.056.08-.106.185-.158.316-.264l-.026-.05c.105-.133.21-.24.263-.344.134-.21.213-.448.318-.685a.385.385 0 0 1 .105-.103c.37.184.37-.21.5-.343.237-.264.474-.553.684-.817.158-.21.316-.395.448-.632.026-.08-.053-.21-.08-.317h-.078c.08-.052.158-.13.237-.184.026 0 .026 0 .052-.026.158-.238.316-.475.474-.686.315-.42.657-.842 1.025-1.21-.052.13-.105.263-.158.368.027 0 .027.027.053.027.316-.422.658-.817.974-1.24-.027-.025-.053-.052-.08-.052-.13.132-.236.264-.368.396-.026-.027-.052-.053-.08-.053.265-.343.528-.685.79-1.08.053.08.106.184.21.395.107-.263.212-.447.29-.632-.078.08-.183.158-.262.238l-.08-.08.474-.71c.5-.712 1-1.45 1.5-2.162.185-.263.42-.474.58-.738.5-1 1.29-1.792 1.894-2.714.132-.184.316-.342.474-.5.13-.16.237-.106.342.026.71.896 1.42 1.818 2.13 2.714.528.66 1.054 1.29 1.554 1.976.605.844 1.184 1.687 1.79 2.53.684.975 1.368 1.95 2.026 2.95 1 1.477 1.947 2.953 2.947 4.428.737 1.08 1.474 2.135 2.184 3.215h-1.344c-1.236-.025-2.5-.13-3.736-.078-1.684.08-3.394.264-5.078.396-2.132.185-4.29.21-6.42.21-.765 0-1.528.107-2.29.16-.922.052-1.817.105-2.738.13-1.08.054-2.13.08-3.21.107-.606.026-1.237 0-1.895 0zm30.183 12.12v.238c-.026 0-.052.027-.105.027-.105-.37-.21-.766-.342-1.135-.263-.765-.553-1.53-1.027-2.214-.528-.737-1-1.5-1.528-2.265-.13-.185-.316-.343-.474-.5-.553-.607-1.106-1.24-1.816-1.687a21.485 21.485 0 0 0-3.29-1.688 7.374 7.374 0 0 1-.92-.474h.63l4.5-.08c.974-.025 1.922-.025 2.895-.078.236 0 .368.08.5.29.236.395.473.79.736 1.186.027.052.08.13.08.21 0 .58 0 1.186.026 1.766.025.606.08 1.186.104 1.792 0 .606-.053 1.238-.026 1.87.027.897.053 1.82.053 2.74zM26.447 26.67c1.237-.053 2.42-.132 3.632-.185.945-.053 1.92-.08 2.866-.132.395-.025.764-.05 1.158 0-.42.212-.842.423-1.21.686-.474.316-.92.737-1.395 1.08-.475.342-.896.764-1.29 1.212-.5.605-1.053 1.132-1.58 1.712-.37.422-.79.817-1.105 1.265-.447.58-.842 1.21-1.263 1.87.132-2.504.29-4.98.184-7.51zm17.185 25.35c-.843.21-1.71.448-2.58.553-.736.106-1.5.08-2.263.08a25.42 25.42 0 0 1-2.028-.08c-.763-.078-1.526-.157-2.263-.5-.633-.29-1.29-.553-1.92-.87-.634-.316-1.265-.684-1.74-1.264-.34-.423-.815-.765-1.236-1.134.08.316.263.58.553.764-.132.158-.316.08-.58-.343-.078.053-.157.08-.21.106.08-.185.158-.37.237-.527-.105-.21-.237-.448-.342-.66-.21-.342-.42-.71-.605-1.053-.053-.08-.053-.158-.105-.237a5.893 5.893 0 0 1-.37-.475c-.21-.315-.394-.657-.657-.974 0 .08.027.158.027.264-.027 0-.053.026-.053.026l-.554-1.344c-.026 0-.026 0-.052.026l.473 1.74c-.026 0-.052.025-.08.025-.077-.104-.156-.21-.21-.34-.052-.212-.21-.212-.34-.133-.08.053-.133.237-.106.316.185.448.395.896.606 1.344.052.158.105.29.184.448.027.053.106.105.106.184.106.21.185.42.316.606.237.316.5.632.737.948.235.316.445.66.656.975.026.053.105.053.13.08.133.395.58.684.896.526.08.606.737.817 1 1.397a11.957 11.957 0 0 1-.763-.343c-.027.026-.027.052-.054.105.316.158.632.316.92.5.265.16.528.317.765.5.316.29.685.45 1.13.554a.282.282 0 0 0-.05-.107c.736.343 1.5.712 2.078 1-2.737.054-5.658.107-8.685.16 0-.5-.026-.975-.026-1.476 0-.21.052-.395.025-.606-.08-1.21-.08-2.424-.237-3.61-.157-1.264-.157-2.503-.13-3.77.025-.683-.027-1.394-.054-2.08 0-.922 0-1.82.028-2.74 0-.132.053-.237.106-.37h.08c.025.054 0 .133.05.16.08.08.212.21.265.184.157-.106.394-.21.447-.37.13-.315.184-.658.184-.974 0-.236.106-.394.21-.553.054-.08.08-.158.133-.263-.105-.08-.21-.132-.342-.237.106-.29.08-.633.475-.79.052-.027.052-.16.08-.238.025-.213.05-.45.078-.66.052.08.08.105.13.157a.42.42 0 0 1 .054-.08c0-.104-.026-.315 0-.315.316-.053.184-.395.342-.553.025-.028-.027-.107-.027-.16 0-.052 0-.13.026-.13.367-.08.315-.475.552-.66.08-.053.105-.13.21-.263.21.368-.158.553-.184.816.446-.263.578-.895.315-1.08.105-.08.21-.184.29-.29.29-.316.604-.606.868-.922.185-.236.29-.526.474-.763.106-.132.316-.237.474-.317.474-.262.92-.552 1.21-1 .053-.053.132-.105.21-.158.08-.053.238-.053.264-.132.027-.052-.052-.184-.105-.263.104-.053.21-.158.42-.264-.08.158-.105.264-.158.37l.13.13c.238-.184.606-.394.843-.552 0-.025-.132-.13-.132-.13-.157.08-.394.21-.63.316.05-.08.05-.132.08-.158.367-.237.735-.474 1.13-.66.92-.42 1.842-.842 2.763-1.237.158-.08.37-.026.553-.026.078 0 .13 0 .21-.026.42-.132.842-.264 1.263-.37.183-.052.393-.078.58-.078.787.025 1.577.025 2.366.078.342.026.658.105.974.21a9.88 9.88 0 0 1 1.184.5c.447.24.868.502 1.29.792.763.5 1.473 1.054 2.236 1.502.737.448 1.316 1.054 1.79 1.74.58.816 1.237 1.554 1.5 2.555l.394 1.74c.08.316.264.632.185 1-.133.66-.238 1.345-.343 2.004-.052.265-.105.53-.078.79.05.82-.265 1.53-.58 2.268-.106.237-.264.475-.395.738a.798.798 0 0 0 .21.106l.237-.474c.027 0 .027 0 .053.027-.132.368-.237.764-.37 1.133-.314.817-.63 1.66-1.025 2.45-.21.448-.58.817-.842 1.24-.262.368-.473.763-.736 1.106-.237.29-.473.58-.79.79-.71.527-1.447 1.054-2.21 1.476-.473.29-1.026.448-1.552.58zm-14.027-1.4l-.026.027c-.055-.026-.134-.052-.186-.105l-.632-.95c-.052-.078-.08-.157-.052-.262.29.448.58.87.895 1.29zm16.37 3.61c1.183-.5 2.157-1.21 3.05-2.028.133-.132.264-.263.422-.37 1.106-.684 1.92-1.633 2.658-2.687.842-1.212 1.395-2.582 2.08-3.873a2.73 2.73 0 0 1 .157-.29c-.053 3.004.29 5.955.684 8.933-2.973.105-6 .21-9.052.316zm26.683-.79c-.026.053-.08.106-.105.16-.027-.054-.027-.133-.053-.24-.158.423-.5.212-.737.212-1.42.027-2.868.027-4.29.027-1.368 0-2.762 0-4.13.024-.448 0-.922.105-1.37.132-1.078.052-2.157.08-3.236.105-.08 0-.158-.13-.29-.236a1.81 1.81 0 0 1-.158.237c-.028-.052-.08-.104-.133-.183-.026.08-.053.158-.08.21H58c-.053-.368-.158-.71-.158-1.08 0-.79.08-1.58.105-2.372.027-.368 0-.71 0-1.054.106.08.185.133.29.21.052-.103.105-.182.158-.26 0 0-.053-.028-.106-.08.05-.027.104-.08.104-.106.026-.08.08-.158.08-.21 0-.185-.054-.343-.08-.5.026 0 .052 0 .08-.028l.157.79h.08c-.106-.183.236-.342-.053-.552-.026-.027.026-.185.026-.264-.08-.157-.13-.315-.21-.526.026-.026.105-.053.184-.08-.105-.052-.184-.104-.263-.13.263-.238.263-.37.026-.633.054-.025.106-.025.106-.05 0-.238 0-.475-.052-.71-.053-.266.08-.58-.316-.74a.79.79 0 0 0 .105.21s-.08.027-.158.08c-.342-.317-.13-.74-.21-1.213.184.053.316.106.447.16-.053-.186-.184-.397-.263-.634h-.107v-1.74c0 .027.184.027.29.054 0-.027.025-.053.025-.08-.08-.105-.185-.21-.29-.342l.053-.053c-.21-.262-.105-.63-.105-.71V39.4c.264.264-.13.606.264.764v-.263h-.027c-.026-.395-.026-.79-.052-1.186h-.052c-.027.054-.027.08-.054.133h-.052l.158-6.298c.263.342.552.66.736 1 .606 1.108 1.395 2.057 2.132 3.058.632.87 1.21 1.818 1.79 2.714.71 1.08 1.394 2.16 2.105 3.24a81.41 81.41 0 0 0 1.63 2.426c.5.71 1.028 1.396 1.554 2.082.446.606.92 1.212 1.367 1.818.527.738 1.053 1.475 1.58 2.187.262.368.552.737.84 1.106.16.21.396.37.554.5-.025 0-.052 0-.104-.026.08.105.13.184.184.237.29.158.316.316.158.554zM74 46.854v-.185c0 .052.026.13 0 .184zm.895-11.62c-.027 0-.184-.16-.21-.186-.027.08 0 .158-.053.264-.027-.078-.21-.052-.21-.13-.027.368.157.737.13 1.106.08-.053.395-.08.474-.158.027.026.08.052.106.052-.527.396-.395.79-.158 1.24.052.104.21.315.052.526-.052.053.027.21.053.343h.077v.05l-.237.08c-.052-.08-.367-.236-.367-.37v1.346c.263.08.263.448.368.633a.768.768 0 0 0 .107-.21l.027.024c-.027.158-.053.316-.106.475-.052.236-.105.447-.13.684 0 .026.05.08.05.105-.288.66-.13 1.396-.235 2.08-.08.5 0 1.03-.053 1.556-.054.448-.16.922-.264 1.37-.027.08-.08.105-.21.158.052-.316.026-.527-.027-.817-.028 0-.37-.184-.397-.184 0 .37.21.87.29 1.29-.08-.026-.395-.21-.42-.21-.054.316-.054.738-.08 1.08-.027.264-.263.5-.29.79 0 .16.184.264.158.528h.21c0-.526.238-1 .238-1.554h.078c.027.053.106.106.08.132-.053.29-.16.606-.132.896 0 .158.13.316.08.5-.054.16-.08.317-.107.554-.027-.132-.053-.184-.053-.263-.026 0-.263-.027-.29-.027-.026.158.185.316.158.448-.026.026-.052.026-.105.053l-.868-1.266c-.686-1-1.37-2.003-2.054-3.03a6.312 6.312 0 0 1-.475-.79 37.09 37.09 0 0 0-2.71-4.033c-.762-.974-1.37-2.03-2.08-3.055-.656-.975-1.314-1.924-1.972-2.9-.237-.315-.526-.605-.737-.948-.683-1.08-1.29-2.187-1.972-3.267-.58-.897-1.21-1.767-1.816-2.636-.21-.29-.42-.607-.632-.923a.37.37 0 0 1-.052-.182c-.053-.58-.106-1.16-.132-1.713 0-.527.053-1.054.053-1.608v-.474c0-.132.025-.237.025-.37.025-.025.052-.078.078-.104-.763 0-1.553-.028-2.316 0-.5.025-.763-.186-1.105-.555-1-1.133-1.737-2.424-2.605-3.636a162.42 162.42 0 0 0-2.5-3.427c-.685-.922-1.37-1.818-2.053-2.74-.764-1.054-1.5-2.108-2.29-3.162a381.983 381.983 0 0 0-2.895-3.794c-.45-.58-.95-1.133-1.45-1.74.343.054.66.106.975.133l1.264.08c.947.077 1.894.13 2.84.26.79.107 1.58.265 2.396.396 1.738.29 3.448.765 5.106 1.318.974.316 1.92.738 2.87 1.133 2.13.87 4.157 1.924 6.157 3.03.63.343 1 .896 1.472 1.397.685.712 1.37 1.423 2.027 2.16.762.87 1.472 1.766 2.21 2.662.657.79 1.34 1.58 2 2.372.21.237.37.527.552.79.42.633.895 1.24 1.263 1.924.262.502.42 1.082.604 1.635.262.817.526 1.607.79 2.424.183.606.34 1.24.472 1.87.106.423.08.87.21 1.29.16.556 0 1.16.16 1.715.025.053.05.132.078.185.105.104.184.21.026.368-.025.026-.025.13 0 .21.054-.052.08-.105.133-.184 0 .053.025.08.025.105 0 .104-.027.21 0 .315 0 .052.052.13.078.184.053-.054.105-.08.21-.16.237.897.264 1.793.264 2.715 0 .87.157 1.74-.21 2.583.078-.29-.106-.555-.027-.818z"/><path d="M58.08 45.482c.025 0 .052.027.052.027l-.027-.03c0-.025 0-.025-.026 0zm4.157 26.036c-.29.21-.58.395-.948.474-.028-.026-.028-.053-.054-.08.29-.184.605-.368.895-.553.027.05.08.104.106.157zM12.895 35.81c.29-.367.58-.736.894-1.105.025.026.235.08.262.105-.29.37-.685.87-.974 1.265-.054-.053-.133-.237-.185-.264zM5.42 48.725c-.21-.448-.42-.923-.63-1.37a.91.91 0 0 1 .236-.106c.29.42.42.92.632 1.37 0 0-.21.105-.237.105zm6.712-12.65c-.158.238-.316.502-.474.74-.026-.028-.316.104-.342.078.158-.237.552-.66.71-.896.027.026.053.053.106.08zM59.422 72.6c.025 0 .025-.026.052-.026.184.026.394.052.605.052-.344.237-.555.21-.66-.026zm-47.24-35.418c.028-.08.08-.158.133-.237.052 0 .13-.027.13-.027.107-.184.107-.316.212-.474-.026-.026-.053-.026-.08-.053-.157.108-.315.24-.473.345.053.052.053.08.053.132-.21-.027-.29.08-.395.368-.026.08-.158.106-.29.21-.026.054-.052.186-.105.317l.027.028c-.053.053-.132.08-.132.08-.158.157-.342.29-.5.447-.026.08-.052.158-.052.237.185-.184.5-.527.737-.738l.027.027c.105-.158.184-.316.29-.474.025.026.025.052.052.08-.08.21-.158.446-.237.657-.055.026-.134.08-.134.053-.105.08-.184.184-.29.263l-.473.316c-.263.237-.526.447-.816.685-.184.29-.368.553-.58.896.317-.08.396.053.37.317.368.052.395-.237.5-.448.026-.054.053-.16.105-.186.237-.21.5-.394.763-.605.053-.053.053-.16.053-.238 0-.026-.133-.026-.212-.053.237-.264.58-.71.816-1 .132-.08.263-.186.263-.265-.026-.29.158-.368.37-.474-.106-.08-.133-.157-.133-.183z"/><path d="M12.71 36.892c-.105.184-.21.342-.315.527l-.158-.08c-.105.605-.474 1.132-.842 1.237.105.053.21.106.29.08.078-.027.13-.16.183-.238l.71-1.028.238-.396-.105-.105zM3.948 48.46c.132 0 .264.026.42.026 0-.105.133-.08.133-.184h.08c0 .132.026.237.026.37h-.552c-.027-.027-.132-.186-.106-.212zm-.21-1.212c-.08-.08-.21-.158-.21-.237-.027-.104.052-.235.13-.367.054.184.08.342.132.527-.027.025-.053.052-.053.078zm.658-1.687c.105.266.21.556.316.82a.798.798 0 0 0-.21.105c-.105-.264-.237-.554-.342-.817a.652.652 0 0 1 .237-.106zm58.58 25.194c.13-.052.288-.08.5-.13-.238.183-.422.315-.58.473-.027-.026-.053-.053-.08-.053.053-.105.106-.184.16-.29zM30.63 15.074c.157-.106.29-.185.447-.29l.052.052c-.16.21-.29.42-.475.685-.026-.183-.026-.29-.053-.42-.026 0 0 0 .027-.026zm7.71 13.333c.237-.106.474-.21.763-.343-.026.158-.026.264-.026.37a.927.927 0 0 0-.264-.054c-.158.027-.448.238-.58.264-.025 0 .106-.21.106-.237zm19.74 22.346c.052.263.552.395.052.658.08.055.157.08.236.134a.2.2 0 0 1-.052.106c-.053.025-.158.078-.21.05-.027 0-.08-.104-.08-.157 0-.237.027-.474.053-.79z"/></g></symbol><symbol viewBox="0 0 24 24" id="powerpoint" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m7 1.5V9h5.5L13 3.5M8 11v2h1v6H8v1h4v-1h-1v-2h2a3 3 0 0 0 3-3 3 3 0 0 0-3-3H8m5 2a1 1 0 0 1 1 1 1 1 0 0 1-1 1h-2v-2h2z" fill="#d14524"/></symbol><symbol viewBox="0 0 67.47 70" id="powershell" xmlns="http://www.w3.org/2000/svg"><path d="M18.545 12.4c-3.014 0-6.08 2.34-6.873 5.248L1.91 53.438c-.793 2.908.996 5.248 4.01 5.248h42.887c3.014 0 6.08-2.34 6.873-5.248l9.761-35.79c.794-2.908-.993-5.248-4.007-5.248h-42.89zm4.848 6.243c.652.04 1.29.33 1.76.86l7.96 9.013-3.957 3.246 3.957-3.244 4.832 5.47c.037.042.06.088.094.131.026.034.057.06.082.096.02.028.032.057.05.086.057.087.105.176.15.267.028.06.055.117.08.178a2.546 2.546 0 0 1 .171.764c.005.073.01.146.008.219-.002.09-.01.178-.021.267a2.53 2.53 0 0 1-.036.217 2.56 2.56 0 0 1-.07.252c-.024.076-.048.15-.08.224a2.547 2.547 0 0 1-.111.22 2.503 2.503 0 0 1-.133.218 2.546 2.546 0 0 1-.147.187c-.058.07-.118.137-.185.202-.027.026-.048.057-.076.082-.037.032-.077.054-.116.084-.038.03-.07.065-.11.093L16.8 52.271a2.552 2.552 0 0 1-3.563-.626 2.553 2.553 0 0 1 .63-3.563l18.349-12.853-3.06-3.467-7.839-8.873a2.549 2.549 0 0 1 .225-3.608 2.546 2.546 0 0 1 1.85-.638zm22.441 28.214c1.377 0 2.255 1.083 1.969 2.43-.287 1.347-1.627 2.433-3.004 2.434l-9.957.006c-1.378 0-2.256-1.083-1.969-2.43.287-1.347 1.626-2.433 3.004-2.434l9.957-.006z" fill="#03a9f4" stroke-width="5.342" stroke-linejoin="round"/></symbol><symbol viewBox="0 0 210 210" id="prettier" xmlns="http://www.w3.org/2000/svg"><title>prettier-icon-dark</title><g transform="matrix(.9 0 0 .9 10.5 10.5)" fill="none" fill-rule="evenodd"><rect fill="#56B3B4" x="165" y="40" width="20" height="10" rx="5"/><rect fill="#EA5E5E" x="15" y="200" width="60" height="10" rx="5"/><rect fill="#BF85BF" x="135" y="120" width="40" height="10" rx="5"/><rect fill="#EA5E5E" x="75" y="120" width="50" height="10" rx="5"/><rect fill="#56B3B4" x="15" y="120" width="50" height="10" rx="5"/><rect fill="#BF85BF" x="15" y="160" width="60" height="10" rx="5"/><rect fill="#BF85BF" x="15" y="80" width="60" height="10" rx="5"/><rect fill="#F7BA3E" x="65" y="20" width="110" height="10" rx="5"/><rect fill="#EA5E5E" x="15" y="20" width="40" height="10" rx="5"/><rect fill="#F7BA3E" x="55" y="180" width="20" height="10" rx="5"/><rect fill="#56B3B4" x="55" y="60" width="20" height="10" rx="5"/><rect fill="#56B3B4" x="15" y="180" width="30" height="10" rx="5"/><rect fill="#F7BA3E" x="15" y="60" width="30" height="10" rx="5"/><rect fill="#56B3B4" x="95" y="100" width="90" height="10" rx="5"/><rect fill="#F7BA3E" x="45" y="100" width="40" height="10" rx="5"/><rect fill="#EA5E5E" x="15" y="100" width="20" height="10" rx="5"/><rect fill="#BF85BF" x="105" y="40" width="50" height="10" rx="5"/><rect fill="#56B3B4" x="15" y="40" width="80" height="10" rx="5"/><rect fill="#F7BA3E" x="45" y="140" width="100" height="10" rx="5"/><rect fill="#BF85BF" x="15" y="140" width="20" height="10" rx="5"/><rect fill="#EA5E5E" x="135" y="60" width="60" height="10" rx="5"/><rect fill="#F7BA3E" x="135" y="80" width="60" height="10" rx="5"/><rect fill="#56B3B4" x="15" width="130" height="10" rx="5"/></g></symbol><symbol viewBox="0 0 80 80" id="protractor" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="hxa"><path transform="scale(1 -1)" fill="#564b55" stroke-width="27.224" d="M-2.983-69.251h69.412v67.108H-2.983z"/></clipPath></defs><g transform="matrix(1.13039 0 0 -1.13039 5.714 82.137)" clip-path="url(#hxa)"><g transform="scale(.1)"><path d="M1180.54 92.324c-5.53 0-9.93-1.797-13.23-5.39-3.29-3.614-5.22-8.594-5.81-14.97h36.02c0 6.583-1.47 11.622-4.4 15.126-2.93 3.496-7.12 5.234-12.58 5.234zm2.84-62.656c-10.19 0-18.22 3.086-24.11 9.297-5.88 6.21-8.83 14.824-8.83 25.84 0 11.101 2.73 19.922 8.21 26.464 5.45 6.524 12.81 9.805 22.02 9.805 8.63 0 15.46-2.851 20.48-8.523 5.03-5.676 7.55-13.157 7.55-22.461v-6.613h-47.45c.21-8.086 2.26-14.22 6.12-18.418 3.89-4.18 9.34-6.29 16.38-6.29 7.42 0 14.76 1.563 22 4.669V34.14c-3.68-1.602-7.18-2.746-10.48-3.438-3.28-.684-7.24-1.035-11.89-1.035M1272.34 30.918v44.57c0 5.606-1.28 9.805-3.82 12.559-2.56 2.773-6.56 4.16-12.02 4.16-7.2 0-12.49-1.953-15.84-5.851-3.34-3.895-5.03-10.32-5.03-19.286V30.918h-10.42v68.887h8.47l1.71-9.422h.5c2.14 3.387 5.14 6.023 8.99 7.887 3.85 1.867 8.15 2.804 12.88 2.804 8.29 0 14.54-2.011 18.73-6.015 4.19-3.985 6.28-10.391 6.28-19.192V30.918h-10.43M1328.96 38.406c7.1 0 12.27 1.938 15.48 5.813 3.22 3.879 4.81 10.129 4.81 18.758v2.199c0 9.765-1.62 16.726-4.87 20.898-3.25 4.18-8.44 6.25-15.56 6.25-6.11 0-10.79-2.383-14.04-7.129-3.26-4.746-4.88-11.472-4.88-20.136 0-8.797 1.61-15.45 4.84-19.93 3.23-4.484 7.97-6.723 14.22-6.723zm20.85 1.762h-.56c-4.83-7.004-12.02-10.5-21.62-10.5-9.01 0-16.03 3.066-21.04 9.238-5 6.153-7.5 14.922-7.5 26.27 0 11.355 2.51 20.176 7.54 26.465 5.03 6.289 12.03 9.433 21 9.433 9.34 0 16.5-3.398 21.49-10.195h.81l-.43 4.96-.25 4.845v28.039h10.43V30.918h-8.49l-1.38 9.25M1434.91 38.27c1.85 0 3.63.136 5.34.421 1.72.274 3.09.547 4.1.84v-7.976c-1.15-.559-2.81-.996-5.01-1.36-2.18-.351-4.17-.527-5.94-.527-13.32 0-19.97 7.012-19.97 21.055V91.71h-9.88v5.027l9.88 4.336 4.38 14.707h6.04V99.805h20V91.71h-20V51.16c0-4.15.98-7.333 2.96-9.56 1.97-2.206 4.67-3.331 8.1-3.331M1463.81 65.43c0-8.809 1.76-15.508 5.27-20.118 3.53-4.609 8.69-6.906 15.53-6.906s12.01 2.297 15.56 6.875c3.53 4.602 5.3 11.301 5.3 20.149 0 8.75-1.77 15.41-5.3 19.953-3.55 4.539-8.77 6.824-15.69 6.824-6.82 0-11.99-2.246-15.47-6.73-3.46-4.48-5.2-11.16-5.2-20.047zm52.47 0c0-11.23-2.83-20-8.48-26.309-5.66-6.309-13.47-9.453-23.44-9.453-6.17 0-11.64 1.445-16.42 4.336-4.78 2.89-8.46 7.031-11.06 12.45-2.59 5.401-3.88 11.73-3.88 18.976 0 11.23 2.8 19.968 8.41 26.242 5.61 6.258 13.4 9.402 23.38 9.402 9.64 0 17.3-3.222 22.97-9.62 5.69-6.415 8.52-15.087 8.52-26.024M1591.71 92.324c-5.54 0-9.94-1.797-13.23-5.39-3.3-3.614-5.24-8.594-5.81-14.97h36c0 6.583-1.46 11.622-4.39 15.126-2.93 3.496-7.13 5.234-12.57 5.234zm2.83-62.656c-10.19 0-18.22 3.086-24.11 9.297-5.89 6.21-8.83 14.824-8.83 25.84 0 11.101 2.74 19.922 8.2 26.464 5.46 6.524 12.81 9.805 22.04 9.805 8.62 0 15.45-2.851 20.48-8.523 5.03-5.676 7.54-13.157 7.54-22.461v-6.613h-47.45c.21-8.086 2.25-14.22 6.13-18.418 3.87-4.18 9.33-6.29 16.36-6.29 7.43 0 14.77 1.563 22.01 4.669V34.14c-3.69-1.602-7.17-2.746-10.46-3.438-3.3-.684-7.27-1.035-11.91-1.035M1683.5 30.918v44.57c0 5.606-1.27 9.805-3.83 12.559-2.55 2.773-6.55 4.16-12.01 4.16-7.2 0-12.48-1.953-15.83-5.851-3.35-3.895-5.03-10.32-5.03-19.286V30.918h-10.43v68.887h8.48l1.69-9.422h.51c2.14 3.387 5.14 6.023 8.99 7.887 3.84 1.867 8.15 2.804 12.88 2.804 8.3 0 14.54-2.011 18.74-6.015 4.19-3.985 6.29-10.391 6.29-19.192V30.918h-10.45M1740.11 38.406c7.12 0 12.28 1.938 15.49 5.813 3.21 3.879 4.81 10.129 4.81 18.758v2.199c0 9.765-1.62 16.726-4.87 20.898-3.25 4.18-8.43 6.25-15.56 6.25-6.12 0-10.8-2.383-14.05-7.129-3.24-4.746-4.88-11.472-4.88-20.136 0-8.797 1.64-15.45 4.85-19.93 3.22-4.484 7.96-6.723 14.21-6.723zm20.87 1.762h-.57c-4.82-7.004-12.03-10.5-21.62-10.5-9.01 0-16.02 3.066-21.03 9.238-5 6.153-7.52 14.922-7.52 26.27 0 11.355 2.52 20.176 7.55 26.465 5.02 6.289 12.02 9.433 21 9.433 9.34 0 16.5-3.398 21.48-10.195h.83l-.44 4.96-.25 4.845v28.039h10.43V30.918h-8.49l-1.37 9.25M1846.07 38.27c1.85 0 3.64.136 5.36.421 1.7.274 3.07.547 4.08.84v-7.976c-1.13-.559-2.8-.996-5-1.36-2.2-.351-4.18-.527-5.94-.527-13.33 0-19.99 7.012-19.99 21.055V91.71h-9.86v5.027l9.86 4.336 4.4 14.707h6.04V99.805H1855V91.71h-19.98V51.16c0-4.15.98-7.333 2.95-9.56 1.97-2.206 4.68-3.331 8.1-3.331M1894.26 92.324c-5.53 0-9.94-1.797-13.22-5.39-3.31-3.614-5.25-8.594-5.83-14.97h36.01c0 6.583-1.45 11.622-4.38 15.126-2.95 3.496-7.13 5.234-12.58 5.234zm2.83-62.656c-10.19 0-18.22 3.086-24.1 9.297-5.9 6.21-8.84 14.824-8.84 25.84 0 11.101 2.73 19.922 8.2 26.464 5.47 6.524 12.81 9.805 22.03 9.805 8.63 0 15.46-2.851 20.49-8.523 5.03-5.676 7.55-13.157 7.55-22.461v-6.613h-47.46c.22-8.086 2.26-14.22 6.13-18.418 3.87-4.18 9.33-6.29 16.37-6.29 7.42 0 14.75 1.563 22 4.669V34.14c-3.7-1.602-7.17-2.746-10.47-3.438-3.28-.684-7.25-1.035-11.9-1.035M1983.36 49.727c0-6.426-2.4-11.368-7.18-14.844-4.77-3.477-11.47-5.215-20.11-5.215-9.13 0-16.26 1.445-21.37 4.336v9.687a51.32 51.32 0 0 1 10.65-3.964c3.79-.977 7.45-1.457 10.97-1.457 5.46 0 9.64.87 12.57 2.609 2.95 1.738 4.41 4.394 4.41 7.95 0 2.694-1.17 4.98-3.5 6.894-2.32 1.914-6.85 4.152-13.6 6.757-6.41 2.383-10.97 4.473-13.67 6.25-2.71 1.778-4.72 3.81-6.04 6.067-1.31 2.254-1.98 4.96-1.98 8.113 0 5.606 2.29 10.04 6.86 13.281 4.57 3.25 10.84 4.883 18.79 4.883 7.42 0 14.66-1.515 21.74-4.531l-3.71-8.496c-6.9 2.851-13.17 4.277-18.79 4.277-4.94 0-8.67-.77-11.18-2.324-2.52-1.543-3.78-3.691-3.78-6.406 0-1.844.48-3.418 1.42-4.707.95-1.309 2.46-2.54 4.56-3.711 2.09-1.184 6.11-2.871 12.07-5.086 8.16-2.98 13.69-5.98 16.55-8.996 2.87-3.02 4.32-6.809 4.32-11.367M2021.28 38.27c1.85 0 3.64.136 5.35.421 1.71.274 3.09.547 4.09.84v-7.976c-1.14-.559-2.81-.996-5.01-1.36-2.18-.351-4.18-.527-5.93-.527-13.33 0-19.99 7.012-19.99 21.055V91.71h-9.87v5.027l9.87 4.336 4.4 14.707h6.02V99.805h20V91.71h-20V51.16c0-4.15 1-7.333 2.97-9.56 1.98-2.206 4.67-3.331 8.1-3.331M2053.61 30.918h-10.42v68.887h10.42zm-11.31 87.559c0 2.39.59 4.14 1.76 5.253 1.18 1.106 2.65 1.661 4.42 1.661 1.67 0 3.1-.567 4.32-1.7 1.22-1.132 1.82-2.871 1.82-5.214 0-2.344-.6-4.09-1.82-5.247-1.22-1.16-2.65-1.726-4.32-1.726-1.77 0-3.24.566-4.42 1.726-1.17 1.157-1.76 2.903-1.76 5.247M2121.59 30.918v44.57c0 5.606-1.27 9.805-3.83 12.559-2.55 2.773-6.55 4.16-12 4.16-7.21 0-12.49-1.953-15.84-5.851-3.35-3.895-5.03-10.32-5.03-19.286V30.918h-10.43v68.887h8.49l1.69-9.422h.5c2.15 3.387 5.14 6.023 8.99 7.887 3.85 1.867 8.16 2.804 12.88 2.804 8.3 0 14.54-2.011 18.74-6.015 4.19-3.985 6.29-10.391 6.29-19.192V30.918h-10.45M2159.29 77.742c0-4.812 1.35-8.465 4.08-10.926 2.72-2.48 6.51-3.71 11.37-3.71 10.19 0 15.28 4.953 15.28 14.831 0 10.344-5.16 15.532-15.47 15.532-4.9 0-8.67-1.32-11.31-3.965-2.63-2.649-3.95-6.555-3.95-11.762zm-5.67-58.387c0-3.73 1.58-6.55 4.72-8.488 3.14-1.922 7.65-2.879 13.52-2.879 8.75 0 15.24 1.309 19.45 3.926 4.21 2.617 6.31 6.172 6.31 10.652 0 3.723-1.15 6.32-3.45 7.754-2.31 1.457-6.65 2.168-13.01 2.168h-12.51c-4.74 0-8.43-1.12-11.06-3.386-2.65-2.266-3.97-5.508-3.97-9.747zm54.94 80.45v-6.582l-12.76-1.512c1.18-1.477 2.23-3.39 3.15-5.754.91-2.371 1.37-5.039 1.37-8.02 0-6.746-2.29-12.128-6.91-16.152-4.61-4.012-10.93-6.023-18.98-6.023-2.05 0-3.98.156-5.78.5-4.45-2.356-6.67-5.305-6.67-8.871 0-1.883.77-3.282 2.34-4.176 1.54-.902 4.21-1.36 7.97-1.36h12.2c7.46 0 13.19-1.574 17.19-4.707 4-3.144 6-7.714 6-13.71 0-7.618-3.06-13.426-9.17-17.43C2192.38 2.004 2183.46 0 2171.72 0c-9 0-15.95 1.68-20.82 5.027-4.88 3.352-7.34 8.079-7.34 14.211 0 4.18 1.35 7.813 4.03 10.88 2.68 3.046 6.45 5.116 11.32 6.21-1.77.8-3.24 2.031-4.44 3.711-1.19 1.68-1.78 3.633-1.78 5.84 0 2.52.66 4.707 2.01 6.602 1.34 1.882 3.44 3.71 6.34 5.468-3.56 1.465-6.46 3.953-8.71 7.48-2.23 3.516-3.35 7.54-3.35 12.06 0 7.55 2.26 13.37 6.79 17.452 4.52 4.082 10.93 6.133 19.22 6.133 3.6 0 6.86-.429 9.75-1.27h23.82M2284.61 91.71h-17.54V30.919h-10.43v60.793h-12.31v4.707l12.31 3.766v3.839c0 16.922 7.4 25.391 22.19 25.391 3.65 0 7.93-.73 12.82-2.195l-2.7-8.364c-4.03 1.301-7.46 1.946-10.31 1.946-3.93 0-6.85-1.309-8.73-3.926-1.89-2.617-2.84-6.816-2.84-12.598v-4.472h17.54V91.71M2302.87 65.43c0-8.809 1.76-15.508 5.28-20.118 3.52-4.609 8.7-6.906 15.52-6.906 6.84 0 12.02 2.297 15.57 6.875 3.54 4.602 5.3 11.301 5.3 20.149 0 8.75-1.76 15.41-5.3 19.953-3.55 4.539-8.78 6.824-15.69 6.824-6.83 0-11.99-2.246-15.46-6.73-3.48-4.48-5.22-11.16-5.22-20.047zm52.48 0c0-11.23-2.82-20-8.47-26.309-5.67-6.309-13.48-9.453-23.46-9.453-6.15 0-11.62 1.445-16.4 4.336-4.77 2.89-8.47 7.031-11.06 12.45-2.59 5.401-3.9 11.73-3.9 18.976 0 11.23 2.81 19.968 8.43 26.242 5.6 6.258 13.4 9.402 23.38 9.402 9.63 0 17.28-3.222 22.97-9.62 5.68-6.415 8.51-15.087 8.51-26.024M2403.79 101.074c3.07 0 5.8-.254 8.22-.761l-1.43-9.676c-2.86.633-5.37.933-7.55.933-5.58 0-10.33-2.261-14.3-6.785-3.95-4.531-5.94-10.156-5.94-16.902V30.918h-10.43v68.887h8.62l1.19-12.754h.5c2.56 4.48 5.63 7.949 9.23 10.37 3.61 2.423 7.56 3.653 11.89 3.653M2500.33 69.766l-10.68 28.476c-1.39 3.594-2.81 8.028-4.28 13.262-.93-4.024-2.24-8.438-3.96-13.262l-10.81-28.476zm14.77-38.848l-11.44 29.227h-36.83l-11.32-29.227h-10.81l36.34 92.273h8.98l36.13-92.273h-11.05M2583.07 30.918v44.57c0 5.606-1.27 9.805-3.83 12.559-2.55 2.773-6.55 4.16-12 4.16-7.21 0-12.49-1.953-15.84-5.851-3.35-3.895-5.03-10.32-5.03-19.286V30.918h-10.43v68.887h8.48l1.69-9.422h.51c2.14 3.387 5.14 6.023 8.99 7.887 3.84 1.867 8.15 2.804 12.88 2.804 8.3 0 14.54-2.011 18.74-6.015 4.19-3.985 6.29-10.391 6.29-19.192V30.918h-10.45M2620.76 77.742c0-4.812 1.36-8.465 4.08-10.926 2.73-2.48 6.53-3.71 11.37-3.71 10.2 0 15.28 4.953 15.28 14.831 0 10.344-5.15 15.532-15.45 15.532-4.91 0-8.68-1.32-11.32-3.965-2.64-2.649-3.96-6.555-3.96-11.762zm-5.66-58.387c0-3.73 1.57-6.55 4.71-8.488 3.15-1.922 7.65-2.879 13.53-2.879 8.75 0 15.23 1.309 19.44 3.926 4.21 2.617 6.31 6.172 6.31 10.652 0 3.723-1.14 6.32-3.45 7.754-2.31 1.457-6.64 2.168-13 2.168h-12.51c-4.74 0-8.43-1.12-11.07-3.386-2.63-2.266-3.96-5.508-3.96-9.747zm54.94 80.45v-6.582l-12.76-1.512c1.18-1.477 2.22-3.39 3.14-5.754.92-2.371 1.38-5.039 1.38-8.02 0-6.746-2.3-12.128-6.92-16.152-4.61-4.012-10.92-6.023-18.97-6.023-2.05 0-3.99.156-5.78.5-4.46-2.356-6.67-5.305-6.67-8.871 0-1.883.78-3.282 2.33-4.176 1.55-.902 4.21-1.36 7.98-1.36h12.2c7.46 0 13.18-1.574 17.18-4.707 4.01-3.144 6-7.714 6-13.71 0-7.618-3.06-13.426-9.17-17.43C2653.87 2.004 2644.94 0 2633.2 0c-9 0-15.95 1.68-20.83 5.027-4.88 3.352-7.33 8.079-7.33 14.211 0 4.18 1.35 7.813 4.02 10.88 2.69 3.046 6.47 5.116 11.32 6.21-1.77.8-3.23 2.031-4.43 3.711-1.19 1.68-1.79 3.633-1.79 5.84 0 2.52.66 4.707 2.01 6.602 1.35 1.882 3.45 3.71 6.35 5.468-3.56 1.465-6.47 3.953-8.71 7.48-2.23 3.516-3.35 7.54-3.35 12.06 0 7.55 2.25 13.37 6.79 17.452 4.52 4.082 10.92 6.133 19.21 6.133 3.62 0 6.86-.429 9.75-1.27h23.83M2692.7 99.805V55.117c0-5.605 1.27-9.805 3.83-12.566 2.56-2.766 6.57-4.145 12.01-4.145 7.2 0 12.47 1.965 15.81 5.903 3.33 3.945 4.99 10.379 4.99 19.304v36.192h10.44V30.918h-8.62l-1.5 9.25h-.58c-2.13-3.41-5.1-5.988-8.88-7.793-3.8-1.809-8.13-2.707-12.99-2.707-8.37 0-14.65 1.992-18.81 5.977-4.18 3.964-6.26 10.351-6.26 19.101v45.059h10.56M2760.61 30.918h10.43v97.805h-10.43zM2810.67 38.27c6.5 0 11.6 1.789 15.31 5.343 3.71 3.575 5.56 8.555 5.56 14.961v6.23l-10.44-.448c-8.3-.286-14.27-1.583-17.94-3.868-3.66-2.273-5.5-5.82-5.5-10.644 0-3.781 1.14-6.64 3.42-8.613 2.29-1.973 5.48-2.961 9.59-2.961zm23.57-7.352l-2.07 9.805h-.51c-3.44-4.305-6.86-7.227-10.27-8.77-3.42-1.523-7.68-2.285-12.8-2.285-6.83 0-12.17 1.758-16.05 5.273-3.87 3.528-5.81 8.536-5.81 15.032 0 13.906 11.12 21.199 33.37 21.875l11.7.359v4.277c0 5.418-1.17 9.395-3.5 11.985-2.32 2.566-6.03 3.855-11.15 3.855-5.74 0-12.24-1.758-19.49-5.273l-3.21 7.988c3.4 1.836 7.11 3.281 11.16 4.324a47.81 47.81 0 0 0 12.16 1.575c8.23 0 14.3-1.817 18.27-5.461 3.96-3.66 5.93-9.5 5.93-17.54V30.919h-7.73M2893.6 101.074c3.07 0 5.8-.254 8.25-.761l-1.46-9.676c-2.84.633-5.35.933-7.54.933-5.56 0-10.33-2.261-14.3-6.785-3.96-4.531-5.93-10.156-5.93-16.902V30.918h-10.44v68.887h8.61l1.19-12.754h.5c2.57 4.48 5.65 7.949 9.25 10.37 3.6 2.423 7.56 3.653 11.87 3.653M2901.63 6.727c-3.94 0-7.04.558-9.31 1.691v9.121c2.97-.84 6.08-1.25 9.31-1.25 4.14 0 7.3 1.25 9.45 3.77 2.16 2.507 3.24 6.132 3.24 10.859v91.895h10.69V31.797c0-7.95-2.01-14.121-6.04-18.496-4.02-4.383-9.8-6.574-17.34-6.574M2999.96 55.371c0-8.086-2.93-14.394-8.8-18.918-5.87-4.52-13.83-6.785-23.88-6.785-10.9 0-19.27 1.406-25.14 4.219v10.3c3.77-1.59 7.88-2.847 12.31-3.765 4.45-.93 8.85-1.399 13.21-1.399 7.12 0 12.49 1.36 16.09 4.063 3.59 2.695 5.4 6.465 5.4 11.277 0 3.196-.63 5.805-1.91 7.832-1.29 2.024-3.42 3.907-6.42 5.625-2.99 1.711-7.56 3.664-13.67 5.84-8.55 3.059-14.66 6.692-18.32 10.871-3.66 4.2-5.51 9.668-5.51 16.407 0 7.089 2.68 12.714 7.99 16.914 5.32 4.191 12.36 6.289 21.12 6.289 9.13 0 17.54-1.68 25.2-5.032l-3.32-9.304c-7.59 3.183-14.96 4.785-22.13 4.785-5.66 0-10.07-1.223-13.26-3.652-3.19-2.43-4.78-5.809-4.78-10.118 0-3.191.59-5.8 1.76-7.832 1.17-2.031 3.14-3.886 5.95-5.597 2.78-1.688 7.04-3.563 12.79-5.625 9.63-3.426 16.26-7.118 19.89-11.063 3.62-3.937 5.43-9.043 5.43-15.332M741.648 375.406h30c28.965 0 50.227 5.039 63.774 15.117 13.531 10.079 20.32 25.821 20.32 47.247 0 19.832-6.074 34.628-18.191 44.402-12.141 9.758-31.028 14.641-56.692 14.641h-39.211zm172.192 64.246c0-36.062-11.809-63.691-35.434-82.898-23.621-19.219-57.234-28.82-100.847-28.82h-35.911V198.73h-56.445v345.329h99.438c43.14 0 75.457-8.829 96.961-26.465 21.496-17.637 32.238-43.614 32.238-77.942M1099.26 464.691c11.17 0 20.39-.789 27.63-2.371l-5.43-51.718c-7.88 1.894-16.07 2.832-24.57 2.832-22.2 0-40.19-7.246-53.97-21.731-13.78-14.48-20.66-33.301-20.66-56.453V198.73h-55.514v261.227h43.464l7.32-46.055h2.83c8.66 15.594 19.96 27.95 33.9 37.09 13.93 9.141 28.93 13.699 45 13.699M1206.88 329.82c0-60.308 22.28-90.465 66.85-90.465 44.08 0 66.13 30.157 66.13 90.465 0 59.688-22.21 89.512-66.61 89.512-23.31 0-40.2-7.707-50.67-23.144-10.47-15.43-15.7-37.54-15.7-66.368zm190.13 0c0-42.672-10.95-75.972-32.83-99.898-21.89-23.945-52.35-35.918-91.41-35.918-24.41 0-45.97 5.508-64.7 16.543-18.75 11.016-33.16 26.836-43.23 47.48-10.08 20.625-15.11 44.551-15.11 71.793 0 42.364 10.86 75.43 32.58 99.2 21.73 23.777 52.36 35.671 91.89 35.671 37.79 0 67.7-12.156 89.75-36.492 22.05-24.328 33.06-57.121 33.06-98.379M1558.11 238.887c13.54 0 27.07 2.129 40.62 6.386v-41.816c-6.13-2.676-14.05-4.922-23.73-6.738-9.69-1.797-19.73-2.715-30.12-2.715-52.59 0-78.88 27.715-78.88 83.144v140.778h-35.68v24.558l38.26 20.325 18.9 55.261h34.26v-58.113h74.39v-42.031h-74.39v-139.84c0-13.379 3.34-23.242 10.03-29.629 6.69-6.387 15.48-9.57 26.34-9.57M1783.44 464.691c11.17 0 20.38-.789 27.62-2.371l-5.43-51.718c-7.88 1.894-16.06 2.832-24.56 2.832-22.2 0-40.2-7.246-53.97-21.731-13.78-14.48-20.66-33.301-20.66-56.453V198.73h-55.52v261.227h43.46l7.34-46.055h2.82c8.66 15.594 19.95 27.95 33.9 37.09 13.92 9.141 28.93 13.699 45 13.699M1925.05 236.523c20.15 0 36.32 5.625 48.52 16.895 12.21 11.25 18.31 27.051 18.31 47.344v22.676l-33.54-1.407c-26.13-.937-45.16-5.312-57.04-13.105-11.89-7.793-17.82-19.727-17.82-35.781 0-11.661 3.45-20.665 10.39-27.051 6.91-6.387 17.32-9.571 31.18-9.571zm82.66-37.793l-11.11 36.387h-1.87c-12.62-15.918-25.29-26.738-38.04-32.48-12.74-5.742-29.13-8.633-49.13-8.633-25.67 0-45.7 6.934-60.1 20.801-14.41 13.847-21.62 33.457-21.62 58.808 0 26.934 10 47.246 30 60.934 19.99 13.691 50.45 21.172 91.41 22.441l45.09 1.414v13.938c0 16.699-3.88 29.16-11.68 37.441-7.79 8.262-19.88 12.383-36.25 12.383-13.39 0-26.23-1.953-38.5-5.891a294.638 294.638 0 0 1-35.44-13.933l-17.94 39.668c14.17 7.41 29.68 13.035 46.52 16.894 16.85 3.868 32.77 5.789 47.72 5.789 33.22 0 58.31-7.246 75.22-21.726 16.94-14.492 25.4-37.246 25.4-68.262V198.73h-39.68M2220.04 194.004c-39.52 0-69.55 11.543-90.1 34.609-20.55 23.067-30.82 56.172-30.82 99.321 0 43.925 10.74 77.707 32.23 101.339 21.5 23.614 52.56 35.418 93.18 35.418 27.56 0 52.35-5.117 74.41-15.359l-16.78-44.641c-23.46 9.133-42.82 13.704-58.1 13.704-45.19 0-67.79-29.993-67.79-89.981 0-29.293 5.63-51.305 16.89-66.031 11.26-14.707 27.76-22.09 49.48-22.09 24.72 0 48.11 6.152 70.15 18.437v-48.417c-9.92-5.84-20.5-10-31.76-12.52-11.26-2.52-24.93-3.789-40.99-3.789M2451.52 238.887c13.54 0 27.08 2.129 40.63 6.386v-41.816c-6.15-2.676-14.05-4.922-23.73-6.738-9.69-1.797-19.73-2.715-30.12-2.715-52.6 0-78.9 27.715-78.9 83.144v140.778h-35.66v24.558l38.26 20.325 18.9 55.261h34.26v-58.113h74.39v-42.031h-74.39v-139.84c0-13.379 3.34-23.242 10.03-29.629 6.69-6.387 15.47-9.57 26.33-9.57M2585.92 329.82c0-60.308 22.28-90.465 66.84-90.465 44.09 0 66.15 30.157 66.15 90.465 0 59.688-22.22 89.512-66.62 89.512-23.31 0-40.2-7.707-50.67-23.144-10.47-15.43-15.7-37.54-15.7-66.368zm190.13 0c0-42.672-10.94-75.972-32.83-99.898-21.89-23.945-52.36-35.918-91.4-35.918-24.42 0-45.98 5.508-64.72 16.543-18.74 11.016-33.14 26.836-43.22 47.48-10.07 20.625-15.12 44.551-15.12 71.793 0 42.364 10.87 75.43 32.59 99.2 21.74 23.777 52.36 35.671 91.89 35.671 37.79 0 67.7-12.156 89.75-36.492 22.04-24.328 33.06-57.121 33.06-98.379M2972.33 464.691c11.18 0 20.38-.789 27.63-2.371l-5.43-51.718c-7.87 1.894-16.05 2.832-24.57 2.832-22.2 0-40.19-7.246-53.96-21.731-13.78-14.48-20.67-33.301-20.67-56.453V198.73h-55.51v261.227h43.46l7.33-46.055h2.83c8.66 15.594 19.96 27.95 33.89 37.09 13.94 9.141 28.94 13.699 45 13.699" fill="#100f0d"/><path d="M610.11 372.83c0-170.584-138.257-308.862-308.846-308.862-170.602 0-308.846 138.278-308.846 308.863 0 170.576 138.244 308.846 308.846 308.846 170.59 0 308.846-138.27 308.846-308.846" fill="#e53935" stroke-width="1.029"/><path d="M460.694 521.792l-105.04.958-61.415 61.415-72.096-47.883 12.445-12.438-29.207.26-99.129-166.817H67.357l24.39-24.402-24.57-41.363L294.66 64.049c2.192-.04 4.399-.08 6.603-.08 170.416 0 308.585 138.055 308.846 308.408L460.694 521.792" fill="#d51c2f" stroke-width="1.029"/><path d="M149.093 350.258c0 84.048 68.13 152.151 152.171 152.151 84.028 0 152.139-68.103 152.139-152.151zm342.063-7.017v14.046h44.015c-1.75 59.337-25.556 113.104-63.54 153.419L438.75 477.81l-9.925 9.94 32.875 32.887c-40.314 37.983-94.081 61.79-153.41 63.527l-.015-44.003h-14.035v44.003c-59.34-1.737-113.096-25.556-153.41-63.527l32.887-32.887-9.945-9.92-32.883 32.875c-37.975-40.315-61.781-94.082-63.53-153.419h44.002l-.008-14.034H67.176v-51.511h468.176v51.5h-44.196" fill="#f5f5f5" stroke-width="1.029"/></g></g></symbol><symbol id="pug" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"><style>.st0{fill:#c1272d}.hyst1{fill:#efcca3}.st2{fill:#ed1c24}.hyst3{fill:#ccac8d}.hyst4{fill:#fff}.st5{fill:#ff931e}.st6{fill:#ffb81e}.hyst7{fill:#56332b}.hyst8{fill:#442823}.hyst9{fill:#7f4a41}.hyst10{fill:#331712}.st11{fill:#fc6}.st12{fill:#ccc}.st13{fill:#b3b3b3}.st14{fill:#989898}.st15{fill:#323232}.st16{fill:#1e1e1e}.st17{fill:#4c4c4c}.st18{fill:#e6e6e6}.st19{fill:#606060}</style><path class="hyst1" d="M107.4 50.9c-.2-4.4.4-8.3-1.6-11.6-4.8-8.2-16.8-13-40.8-13v.7h-.5.5v-.7c-24 0-36.6 4.8-41.4 13.1-1.9 3.4-1.7 7.2-2 11.6-.2 3.5-1.8 7.2-1.1 11.2.8 5.2 1.1 10.4 1.9 15.2.6 3.9 6 7.2 6.5 10.9 1.4 10.2 12 14.9 36 14.9v.8h-.6.7v-.8c24 0 34.2-4.7 35.5-14.9.5-3.8 5.5-7 6.1-10.9.8-4.8 1.1-10 1.9-15.2.7-4-.9-7.8-1.1-11.3z"/><path class="hyst3" d="M64.6 54.5c4.3.1 7.3 2.8 10.1 5.3 3.3 2.9 8.9 4.9 11.2 7.4 2.3 2.5 5.3 5 6.4 8.9 1.1 3.9 1.4 8.9 1.4 10.2 0 1.3.7 1 2.7 0 4.7-2.3 9.9-8.5 9.9-8.5-.6 3.9-5.7 7.4-6.2 11.1C98.9 99.1 89 104 64.5 104h-.1.6"/><path class="hyst3" d="M80.4 46.7c.9 3.1 4.1 13.6-2.1 10.1 0 0 2.6 1.5 4.2 7.2 1.7 5.7 5.8 6.4 5.8 6.4s6.7 1.3 11.7-3c4.2-3.6 4.9-10 3.1-14.9-1.8-4.8-5-6.3-9.7-7.3-4.7-1.1-14.1-2-13 1.5z"/><circle cx="92.3" cy="58.1" r="8.8"/><circle class="hyst4" cx="90" cy="54.2" r="2.3"/><path class="hyst1" d="M78.9 57.7s7.9 5.4 12.2 10.7c4.3 5.3 4.2 6.3 4.2 6.3l-3.1 1.4s-4.4-8.3-9.8-11.4c-5.5-3.1-6.1-5.7-6.1-5.7l2.6-1.3z"/><path class="hyst3" d="M64.9 54.5c-4.3.1-7.5 2.8-10.4 5.3-3.3 2.9-9.1 4.9-11.4 7.4-2.3 2.5-5.4 5-6.5 8.9-1.1 3.9-1.5 8.9-1.5 10.2 0 1.3.2 1.4-2.7 0-4.7-2.2-9.9-8.5-9.9-8.5.6 3.9 5.7 7.4 6.2 11.1C30.1 99.1 40 104 64.5 104h.5"/><path class="hyst7" d="M88.1 71.4C83.3 65.5 75.6 60 64.9 60h-.1c-10.7 0-18.4 5.5-23.2 11.4-5 6.1-4.6 8.5-4.6 14.3 0 21 7.4 15 12.3 17.6 5 2.5 10.2 1.7 15.5 1.7h.1c5.4 0 10.5.7 15.5-1.8 4.9-2.5 12.3 3.7 12.3-17.3.1-5.8.4-8.4-4.6-14.5z"/><path class="hyst8" d="M64.4 65.2s-.7 9.7-2.1 11.6l2.6-.6-.5-11z"/><path class="hyst8" d="M65.1 65.2s.7 9.7 2.1 11.6l-2.6-.6.5-11z"/><path class="hyst7" d="M56.7 62.9c-1-2.3 2.6-6 8.3-6.1 5.7 0 9.3 3.7 8.3 6.1-1 2.4-4.6 3.1-8.3 3.2-3.6-.1-7.3-.8-8.3-3.2z"/><path d="M65 65.2c0-.4 3.4-.5 5.2-1.7 0 0-3.7 1.2-4.5.7-.8-.4-1-1.6-1-1.6s-.3 1.2-.9 1.6c-.7.4-4.9-.7-4.9-.7s5.6 1.4 5.6 1.7c0 .3-.1 1.3-.1 2 0 2.5 0 8.7.4 9.2.6.9.4-6.7.4-9.2-.1-.8-.1-1.6-.2-2z"/><path class="hyst9" d="M65.2 78.6c1.7 0 4.7 1.2 7.4 3.1-2.6-2.9-5.7-4.9-7.4-4.9-1.8 0-5.6 2.2-8.3 5.4 2.8-2.2 6.4-3.6 8.3-3.6z"/><path class="hyst8" d="M64.5 96.3c-3.8 0-7.5-1.2-10.9-2.1-.7-.2-1.4.3-2.1.1-6.3-2-11.4-5.4-14.5-9.7v1c0 21 7.4 15.1 12.3 17.6 5 2.5 10.2 1.7 15.5 1.7h.1c5.4 0 10.5.7 15.5-1.8 4.9-2.5 12.3 3.6 12.3-17.4 0-.8 0-1.6.1-2.3-2.9 4.7-8.2 8.4-14.8 10.6-.6.2-2-.3-2.6-.2-3.6 1.2-6.8 2.5-10.9 2.5z"/><path class="hyst8" d="M55 85s-2.5 7.5-.8 10.8l-2.3-1s1.7-7.6 3.1-9.8zM74.8 85s2.5 7.5.8 10.8l2.3-1s-1.8-7.6-3.1-9.8z"/><path class="hyst3" d="M48.6 46.7c-.9 3.1-4.1 13.6 2.1 10.1 0 0-2.6 1.5-4.2 7.2s-5.8 6.4-5.8 6.4-6.7 1.3-11.7-3c-4.2-3.6-4.9-10-3.1-14.9s5-6.3 9.7-7.3c4.7-1.1 14-2 13 1.5z"/><path d="M64.9 76.8c2.7 0 11.1 5.8 11.2 12.9v-.4c0-7.4-6.8-13.3-11.2-13.3-4.4 0-11.2 6-11.2 13.3v.4c.1-7.1 8.5-12.9 11.2-12.9z"/><ellipse transform="rotate(-14.465 66.712 61.468)" class="hyst10" cx="66.7" cy="61.5" rx=".8" ry="1.5"/><ellipse transform="rotate(17.235 62.371 61.462)" class="hyst10" cx="62.4" cy="61.5" rx=".8" ry="1.5"/><circle cx="37.2" cy="58.1" r="8.8"/><circle class="hyst4" cx="39.5" cy="54.2" r="2.3"/><path class="hyst9" d="M67.5 58.2c0-.1-2.3 1-2.9 1.1-.6-.1-2.9-1.2-2.9-1.1h5.8z"/><path class="hyst1" d="M50 57.7s-7.9 5.4-12.2 10.7c-4.3 5.3-4.2 6.3-4.2 6.3l3.1 1.4s4.4-8.3 9.8-11.4 6.1-5.7 6.1-5.7L50 57.7z"/><path class="hyst3" d="M32.7 41.7S30 49.1 24 52.2c0 0 9.4-1.1 8.7-10.5zM95.8 41.7s2.7 7.4 8.7 10.5c0 0-9.4-1.1-8.7-10.5zM78.7 55.5s-5.9-6.2-13.8-6.4h.1.1c-8 .2-13.8 6.4-13.8 6.4 6.9-4.8 12.8-4.7 13.8-4.7-.1 0 6.7-.1 13.6 4.7zM71.8 42.5s-3-4.2-7-4.3h.2c-3 .1-6.9 4.3-6.9 4.3 3.4-3.3 6.9-3.2 6.9-3.2s3.3-.1 6.8 3.2zM37.2 73.2s-4.7 2.3-8.1.9H29c-3-1.7-4.5-6.8-4.5-6.8s3 9 12.7 5.9zM92 73.2s4.7 2.3 8.1.9c4-1.7 4.6-6.8 4.6-6.8s-3 9-12.7 5.9z"/><path class="hyst3" d="M42.6 41.2c2.6-.5 6.9-.6 10.3.5 4.3 1.5.8 7 1.7 7.3.9.3 2.1-3.8 10.1-3.4 8.1.4 9 4 10.1 3.4s-1.1-10 11-7.8c0 0-12.7-3.4-12.1 5.8 0 0-7.3-5.6-17.5-.6.1 0 2.7-8.6-13.6-5.2zM86.9 41.2c.2 0 .3.1.4.1.1 0-.1-.1-.4-.1zM86.9 41.2zM39.1 28.9S28.3 42.5 26.7 47.7c-1.6 5.3-2.8 27-4.2 30.1l-5-21.4 9.2-22.3 12.4-5.2zM89.9 28.9s10.8 13.6 12.4 18.8c1.6 5.3 2.8 27 4.2 30.1l5-21.4-9.2-22.3-12.4-5.2z"/><path class="hyst7" d="M89.4 28.9s11.6 9.7 15 20.9c3.4 11.2 2 24.8 4.6 26.5 3.7 2.4 7.9-11.9 9.3-13.4 2.2-2.4 9.5-8.5 10-9.6.5-1.1-14.8-17.8-21.5-21.1-8.1-3.8-18.1-4.1-17.4-3.3z"/><path class="hyst8" d="M99.3 34.9s13.7 17.5 13.5 39.3l5.5-11.2c-.1 0-4.9-14.3-19-28.1z"/><path class="hyst7" d="M39.1 28.9s-11.6 9.7-15 20.9-2 24.8-4.6 26.5c-3.7 2.4-7.9-11.9-9.3-13.4C8 60.5.7 54.4.2 53.3-.3 52.2 15 35.5 21.7 32.2c8.1-3.8 18.1-4.1 17.4-3.3z"/><path class="hyst8" d="M29.2 34.9S15.5 52.4 15.7 74.2L10.3 63s4.8-14.3 18.9-28.1z"/><path class="hyst3" d="M21.8 74.6s1 5.4 2.6 7.1.5-1.3.5-1.3-1.7-.9-1.4-7.8-1.7 2-1.7 2zM107.1 74.6s-1 5.4-2.6 7.1-.5-1.3-.5-1.3 1.7-.9 1.4-7.8 1.7 2 1.7 2z"/><g><circle class="hyst8" cx="54.5" cy="70.5" r=".8"/><circle class="hyst8" cx="49.9" cy="75.3" r=".8"/><circle class="hyst8" cx="48.4" cy="70.5" r=".8"/></g><g><circle class="hyst8" cx="74" cy="70.5" r=".8"/><circle class="hyst8" cx="78.6" cy="75.3" r=".8"/><circle class="hyst8" cx="80.1" cy="70.5" r=".8"/></g></symbol><symbol viewBox="0 0 50 50" id="puppet" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -247)" fill="#fbc02d"><path stroke-width=".283" d="M11.559 249.467h13.587v13.587H11.559zM27.435 265.056h13.587v13.587H27.435zM11.559 281.074h13.587v13.587H11.559z"/><path stroke-width=".256" d="M16.62 251.615l18.305 18.305-3.236 3.236-18.305-18.305z"/><path stroke-width=".256" d="M37.834 271.331L19.53 289.636l-3.237-3.237 18.305-18.304z"/></g></symbol><symbol viewBox="0 0 100 99.999997" id="purescript" xmlns="http://www.w3.org/2000/svg"><path clip-path="url(#SVGID_2_)" d="M98.079 38.548L79.22 19.68l-5.087 5.088L90.447 41.09 74.134 57.41l5.087 5.087 18.858-18.86a3.59 3.59 0 0 0 1.055-2.55 3.578 3.578 0 0 0-1.055-2.54M25.483 42.794l-5.09-5.089L1.53 56.568a3.566 3.566 0 0 0-1.05 2.545c0 .961.373 1.863 1.05 2.542L20.394 80.52l5.089-5.086L9.162 59.113z" fill="#42a5f5" stroke-width="1.192"/><path clip-path="url(#SVGID_2_)" transform="matrix(1.19175 0 0 1.19175 -306.84 -629.047)" fill="#42a5f5" d="M281.841 551.736l6.461 6.037h28.379l-6.461-6.037zM288.302 566.861l-6.463 6.035h28.381l6.463-6.035zM281.838 581.982l6.464 6.035h28.381l-6.463-6.035z"/></symbol><symbol viewBox="0 0 24 24" id="python" xmlns="http://www.w3.org/2000/svg"><path d="M19.14 7.5A2.86 2.86 0 0 1 22 10.36v3.78A2.86 2.86 0 0 1 19.14 17H12c0 .39.32.96.71.96H17v1.68a2.86 2.86 0 0 1-2.86 2.86H9.86A2.86 2.86 0 0 1 7 19.64v-3.75a2.85 2.85 0 0 1 2.86-2.85h5.25a2.85 2.85 0 0 0 2.85-2.86V7.5h1.18m-4.28 11.79c-.4 0-.72.3-.72.89 0 .59.32.71.72.71a.71.71 0 0 0 .71-.71c0-.59-.32-.89-.71-.89m-10-1.79A2.86 2.86 0 0 1 2 14.64v-3.78A2.86 2.86 0 0 1 4.86 8H12c0-.39-.32-.96-.71-.96H7V5.36A2.86 2.86 0 0 1 9.86 2.5h4.28A2.86 2.86 0 0 1 17 5.36v3.75a2.85 2.85 0 0 1-2.86 2.85H8.89a2.85 2.85 0 0 0-2.85 2.86v2.68H4.86M9.14 5.71c.4 0 .72-.3.72-.89 0-.59-.32-.71-.72-.71-.39 0-.71.12-.71.71s.32.89.71.89z"/><path d="M9.264 22.379c-.895-.24-1.581-.799-1.947-1.582-.228-.489-.237-.606-.238-2.957-.001-2.745.057-3.074.666-3.785.193-.226.568-.517.833-.648.47-.23.579-.239 3.839-.288 3.131-.048 3.386-.065 3.814-.264.626-.291 1.07-.687 1.4-1.247.27-.46.278-.522.311-2.29l.034-1.82.932.051c1.075.058 1.504.211 2.098.748.853.77.869.841.869 3.957 0 2.434-.02 2.783-.18 3.075a3.365 3.365 0 0 1-1.337 1.33l-.517.273-3.95.031-3.951.031.068.274c.037.151.164.377.282.503.209.224.262.229 2.433.229h2.22v1.05c0 1.653-.394 2.437-1.54 3.072l-.545.302-2.644.018c-1.455.01-2.782-.018-2.95-.063zm6.12-1.692c.22-.222.253-.325.206-.675-.07-.523-.278-.73-.732-.73-.467 0-.672.217-.735.78-.042.372-.012.496.163.672.3.3.77.28 1.097-.047z" fill="#fc0" stroke="#fc0" stroke-width=".102"/><path d="M9.349 22.38c-.911-.15-1.936-1.074-2.176-1.963-.073-.273-.101-1.279-.079-2.868.033-2.317.047-2.473.27-2.926.13-.263.401-.623.603-.8.674-.592.87-.63 3.484-.675 4.399-.076 4.927-.166 5.705-.967.642-.662.706-.9.774-2.883l.061-1.784.951.055c.523.031 1.11.122 1.304.204.54.225 1.358 1.042 1.472 1.47.153.572.243 3.18.16 4.617-.071 1.23-.093 1.327-.395 1.78-.193.288-.577.647-.966.903l-.647.425-3.922.008c-2.157.004-3.942.028-3.966.052-.115.115.354.82.587.883.14.038 1.181.073 2.314.079l2.06.01v.91c0 1.739-.326 2.446-1.454 3.162l-.631.4-2.543-.011c-1.398-.007-2.733-.043-2.966-.081zm5.98-1.718c.285-.256.313-.328.251-.658-.09-.483-.301-.682-.722-.682-.436 0-.625.193-.715.73-.065.384-.044.453.2.663.358.308.595.295.985-.053z" fill="#fdd835" stroke-width=".102"/><path d="M4.281 17.396c-.88-.215-1.714-.935-2.024-1.747-.149-.389-.168-.804-.142-3.041.027-2.26.054-2.638.215-2.962.259-.519.851-1.092 1.392-1.346.437-.206.632-.217 4.408-.245l3.95-.03-.067-.275a1.367 1.367 0 0 0-.282-.504c-.21-.224-.263-.23-2.433-.23h-2.22l.002-1.143c.003-1.338.157-1.795.84-2.493.746-.763 1.103-.838 4.025-.838 2.961 0 3.28.06 4.067.768.37.333.572.621.728 1.037.201.539.213.735.183 3.072-.035 2.777-.045 2.824-.78 3.598-.787.829-.76.824-4.59.883-3.812.06-3.797.057-4.61.806-.765.706-.917 1.2-.964 3.133l-.04 1.653-.677-.01c-.371-.007-.813-.045-.98-.086zM9.59 5.551c.237-.204.286-.326.286-.72 0-.547-.201-.763-.71-.763-.502 0-.765.248-.765.724 0 .492.141.782.439.902.345.14.444.12.75-.143z" fill="#3c78aa"/></symbol><symbol viewBox="0 0 24 24" id="r" xmlns="http://www.w3.org/2000/svg"><path d="M11.956 4.05c-5.694 0-10.354 3.106-10.354 6.947 0 3.396 3.686 6.212 8.531 6.813v2.205h3.53V17.82c.88-.093 1.699-.259 2.475-.497l1.43 2.692h3.996l-2.402-4.048c1.936-1.263 3.147-3.034 3.147-4.97 0-3.841-4.659-6.947-10.354-6.947m1.584 2.712c4.349 0 7.558 1.45 7.558 4.753 0 1.77-.952 3.013-2.505 3.779a1.081 1.081 0 0 1-.228-.156c-.373-.165-.994-.352-.994-.352s3.085-.227 3.085-3.302-3.23-3.127-3.23-3.127h-7.092v7.413c-2.64-.766-4.462-2.392-4.462-4.255 0-2.63 3.52-4.753 7.868-4.753m.156 4.12h2.143s.983-.05.983.974c0 1.004-.983 1.004-.983 1.004h-2.143v-1.977m-.031 4.566h.952c.186 0 .28.052.445.207.135.103.28.3.404.476-.57.073-1.17.104-1.801.104z" fill="#1976d2" stroke-width="1.035"/></symbol><symbol viewBox="0 0 24 24" id="raml" xmlns="http://www.w3.org/2000/svg"><path d="M5 3h2v2H5v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5h2v2H5c-1.07-.27-2-.9-2-2v-4a2 2 0 0 0-2-2H0v-2h1a2 2 0 0 0 2-2V5a2 2 0 0 1 2-2m14 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3h2m-7 12a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m-4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="razor" xmlns="http://www.w3.org/2000/svg"><path d="M15.45 11.91c-.11-2.21-1.75-3.54-3.73-3.54h-.08c-2.29 0-3.55 1.8-3.55 3.84 0 2.29 1.53 3.74 3.54 3.74 2.25 0 3.72-1.65 3.83-3.59m-3.81-5.97c1.53 0 2.97.68 4.02 1.74 0-.51.33-.89.83-.89h.11c.74 0 .89.7.89.92v7.9c-.04.52.54.78.87.44 1.27-1.29 2.78-6.69-.79-9.81-3.33-2.92-7.8-2.44-10.18-.8-2.52 1.74-4.14 5.61-2.57 9.22 1.71 3.95 6.61 5.13 9.52 3.95 1.48-.59 2.15 1.4.65 2.05-2.34.99-8.77.89-11.78-4.32-2.03-3.52-1.93-9.71 3.46-12.92C10.81 1.42 16.24 2.1 19.5 5.5c3.45 3.6 3.25 10.3-.1 12.91-1.51 1.18-3.76.03-3.74-1.7l-.02-.56a5.611 5.611 0 0 1-3.99 1.66C8.63 17.81 6 15.15 6 12.13c0-3.05 2.63-5.74 5.65-5.74z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="react" xmlns="http://www.w3.org/2000/svg"><path d="M12 10.11c1.03 0 1.87.84 1.87 1.89 0 1-.84 1.85-1.87 1.85-1.03 0-1.87-.85-1.87-1.85 0-1.05.84-1.89 1.87-1.89M7.37 20c.63.38 2.01-.2 3.6-1.7-.52-.59-1.03-1.23-1.51-1.9a22.7 22.7 0 0 1-2.4-.36c-.51 2.14-.32 3.61.31 3.96m.71-5.74l-.29-.51c-.11.29-.22.58-.29.86.27.06.57.11.88.16l-.3-.51m6.54-.76l.81-1.5-.81-1.5c-.3-.53-.62-1-.91-1.47C13.17 9 12.6 9 12 9c-.6 0-1.17 0-1.71.03-.29.47-.61.94-.91 1.47L8.57 12l.81 1.5c.3.53.62 1 .91 1.47.54.03 1.11.03 1.71.03.6 0 1.17 0 1.71-.03.29-.47.61-.94.91-1.47M12 6.78c-.19.22-.39.45-.59.72h1.18c-.2-.27-.4-.5-.59-.72m0 10.44c.19-.22.39-.45.59-.72h-1.18c.2.27.4.5.59.72M16.62 4c-.62-.38-2 .2-3.59 1.7.52.59 1.03 1.23 1.51 1.9.82.08 1.63.2 2.4.36.51-2.14.32-3.61-.32-3.96m-.7 5.74l.29.51c.11-.29.22-.58.29-.86-.27-.06-.57-.11-.88-.16l.3.51m1.45-7.05c1.47.84 1.63 3.05 1.01 5.63 2.54.75 4.37 1.99 4.37 3.68 0 1.69-1.83 2.93-4.37 3.68.62 2.58.46 4.79-1.01 5.63-1.46.84-3.45-.12-5.37-1.95-1.92 1.83-3.91 2.79-5.38 1.95-1.46-.84-1.62-3.05-1-5.63-2.54-.75-4.37-1.99-4.37-3.68 0-1.69 1.83-2.93 4.37-3.68-.62-2.58-.46-4.79 1-5.63 1.47-.84 3.46.12 5.38 1.95 1.92-1.83 3.91-2.79 5.37-1.95M17.08 12c.34.75.64 1.5.89 2.26 2.1-.63 3.28-1.53 3.28-2.26 0-.73-1.18-1.63-3.28-2.26-.25.76-.55 1.51-.89 2.26M6.92 12c-.34-.75-.64-1.5-.89-2.26-2.1.63-3.28 1.53-3.28 2.26 0 .73 1.18 1.63 3.28 2.26.25-.76.55-1.51.89-2.26m9 2.26l-.3.51c.31-.05.61-.1.88-.16-.07-.28-.18-.57-.29-.86l-.29.51m-2.89 4.04c1.59 1.5 2.97 2.08 3.59 1.7.64-.35.83-1.82.32-3.96-.77.16-1.58.28-2.4.36-.48.67-.99 1.31-1.51 1.9M8.08 9.74l.3-.51c-.31.05-.61.1-.88.16.07.28.18.57.29.86l.29-.51m2.89-4.04C9.38 4.2 8 3.62 7.37 4c-.63.35-.82 1.82-.31 3.96a22.7 22.7 0 0 1 2.4-.36c.48-.67.99-1.31 1.51-1.9z" fill="#00bcd4"/></symbol><symbol viewBox="0 0 24 24" id="readme" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="reason" xmlns="http://www.w3.org/2000/svg"><path d="M3 3v18h18V3H3zm5.119 8.993h2.798c.382 0 .71.025.985.075.275.05.534.159.774.326.244.168.435.386.577.654.145.265.218.598.218 1 0 .552-.112 1.001-.335 1.35-.22.348-.536.638-.947.87l2.16 3.203H12.31l-1.763-2.742h-.77v2.742H8.12v-7.478zm6.594 0h4.676v1.447h-3.018v1.29h2.802v1.447h-2.802v1.848h3.018v1.446h-4.676v-7.478zM9.778 13.37v2.014h.513c.266 0 .49-.014.67-.044.18-.03.329-.1.45-.207a.96.96 0 0 0 .253-.34c.055-.128.082-.297.082-.508 0-.187-.034-.35-.1-.483a.698.698 0 0 0-.343-.317 1.086 1.086 0 0 0-.395-.095 6.012 6.012 0 0 0-.526-.02h-.604z" fill="#f44336" stroke-width="1.067"/></symbol><symbol viewBox="0 0 172 193" id="restql" xmlns="http://www.w3.org/2000/svg"><title>Group</title><g transform="translate(14.767 16.713) scale(.82795)" fill="none"><path d="M171.39 55.799c-.975-6.147-4.673-11.642-10.15-14.805L96.381 3.546C93.217 1.72 89.615.756 85.964.756s-7.253.964-10.415 2.788L10.69 40.992A20.896 20.896 0 0 0 .272 59.035v74.89a20.894 20.894 0 0 0 10.416 18.042l64.859 37.446c3.165 1.827 6.767 2.791 10.417 2.791s7.252-.964 10.415-2.79l64.859-37.445c5.479-3.166 9.178-8.66 10.152-14.808zm-16.516 85.147L90.017 178.39a8.104 8.104 0 0 1-8.108 0l-64.857-37.444a8.109 8.109 0 0 1-4.053-7.021v-74.89a8.109 8.109 0 0 1 4.053-7.021l64.857-37.446c1.254-.725 2.654-1.086 4.054-1.086s2.8.361 4.054 1.086l64.857 37.446a8.106 8.106 0 0 1 4.053 7.021v74.89a8.109 8.109 0 0 1-4.053 7.021z" fill="#83e8c2"/><path d="M158.93 59.035a8.109 8.109 0 0 0-4.053-7.021L90.02 14.568c-1.254-.725-2.654-1.086-4.054-1.086s-2.8.361-4.054 1.086L17.055 52.014a8.106 8.106 0 0 0-4.053 7.021v74.89a8.109 8.109 0 0 0 4.053 7.021l64.857 37.444a8.104 8.104 0 0 0 8.108 0l64.857-37.444a8.109 8.109 0 0 0 4.053-7.021zm-46.766 31.681c.119-.069.242-.118.365-.149.044-.012.088-.01.131-.018.076-.012.152-.029.228-.029l.015.001c.02.001.038.005.059.006.093.005.184.019.273.04l.1.03c.077.025.15.057.223.095.028.014.057.027.084.043.094.057.184.122.263.199.007.008.013.017.021.024.07.071.133.15.188.235.018.029.033.059.05.09.04.072.072.148.099.229a1.512 1.512 0 0 1 .081.46v16.209l-3.278 1.893a1.548 1.548 0 0 0-.678.83 1.533 1.533 0 0 0-.098.514v3.785l-14.038 8.104-.01.004a1.55 1.55 0 0 1-.354.146c-.045.012-.09.011-.135.018-.074.012-.15.029-.225.029l-.014-.001c-.02-.001-.039-.005-.059-.006a1.463 1.463 0 0 1-.273-.041c-.034-.008-.066-.019-.1-.03a1.318 1.318 0 0 1-.223-.094c-.029-.015-.057-.027-.084-.044a1.45 1.45 0 0 1-.263-.198c-.009-.008-.015-.019-.023-.027a1.495 1.495 0 0 1-.185-.232c-.019-.029-.034-.06-.051-.09a1.422 1.422 0 0 1-.098-.229 1.702 1.702 0 0 1-.033-.101 1.487 1.487 0 0 1-.048-.358l-.001-.002v-20.053a1.446 1.446 0 0 1 .727-1.255zM85.24 31.369a1.449 1.449 0 0 1 1.452 0l45.741 26.41a1.45 1.45 0 0 1 0 2.512l-17.366 10.027a1.457 1.457 0 0 1-1.452 0l-15.49-8.943 1.727-.996a1.552 1.552 0 0 0 0-2.688l-13.111-7.57c-.239-.139-.508-.207-.775-.207s-.535.068-.775.207l-3.278 1.893-14.038-8.104a1.451 1.451 0 0 1 0-2.513zM57.59 47.558c.251 0 .501.065.726.194l15.489 8.942-1.727.997a1.552 1.552 0 0 0 0 2.688l1.727.996-15.488 8.943a1.457 1.457 0 0 1-1.452 0L39.499 60.291a1.45 1.45 0 0 1 0-2.512l17.366-10.027c.225-.129.475-.194.725-.194zm-9.56 92.328c-.241 0-.489-.062-.724-.196l-17.365-10.026a1.45 1.45 0 0 1-.726-1.256V75.59c0-.847.694-1.453 1.452-1.453.242 0 .49.062.724.197l17.366 10.025c.449.26.726.738.726 1.257v17.886l-1.727-.997a1.552 1.552 0 0 0-2.327 1.344v15.139c0 .555.295 1.067.775 1.344l3.278 1.894v16.209a1.45 1.45 0 0 1-1.452 1.451zm29.828 14.929a1.452 1.452 0 0 1-2.177 1.257l-17.365-10.026a1.452 1.452 0 0 1-.726-1.257v-17.885l1.726.996c.25.145.515.211.773.211.811 0 1.554-.648 1.554-1.555v-1.993l15.489 8.942c.449.26.726.738.726 1.257zm0-32.768c0 .127-.02.246-.049.36-.009.035-.021.067-.032.101-.026.08-.059.157-.099.229-.017.03-.032.061-.05.09a1.48 1.48 0 0 1-.188.235l-.021.025a1.51 1.51 0 0 1-.264.199c-.026.016-.055.028-.082.043a1.597 1.597 0 0 1-.324.124 1.362 1.362 0 0 1-.278.041c-.018.001-.036.006-.055.006l-.015.001c-.077 0-.155-.018-.233-.03-.043-.007-.084-.005-.125-.017a1.484 1.484 0 0 1-.366-.149l-14.035-8.104v-3.784a1.545 1.545 0 0 0-.776-1.343l-3.276-1.892V91.976c0-.127.02-.246.049-.361.009-.034.021-.066.032-.1a1.33 1.33 0 0 1 .099-.229c.017-.03.032-.062.051-.091.054-.084.116-.163.187-.234l.021-.025c.079-.076.168-.142.263-.199.027-.016.056-.029.084-.043a1.476 1.476 0 0 1 .601-.166c.019 0 .036-.005.055-.005l.015-.001c.078 0 .157.018.236.03.04.007.081.005.122.017.124.031.246.08.366.149l17.361 10.023a1.456 1.456 0 0 1 .726 1.259zm-9.984-45.373a1.448 1.448 0 0 1-.544-.55 1.466 1.466 0 0 1 0-1.413c.121-.219.303-.41.544-.55l14.038-8.104 3.277 1.892c.48.276 1.071.276 1.551 0l3.278-1.893 14.038 8.105a1.45 1.45 0 0 1 0 2.513L86.691 86.7a1.447 1.447 0 0 1-1.452 0zm74.842 51.733c0 .518-.276.997-.726 1.256l-45.741 26.409a1.452 1.452 0 0 1-2.177-1.257v-20.053c0-.519.277-.997.727-1.257l15.488-8.941v1.992c0 .906.743 1.555 1.553 1.555.26 0 .523-.066.774-.21l13.11-7.57a1.55 1.55 0 0 0 .776-1.344v-3.784l14.038-8.105a1.452 1.452 0 0 1 2.177 1.257v20.052zm0-32.764c0 .519-.276.997-.726 1.256l-15.489 8.943v-1.993c0-.906-.744-1.554-1.554-1.554a1.519 1.519 0 0 0-.773.21l-1.727.996V85.616c0-.519.277-.997.727-1.257l17.365-10.025c.234-.135.482-.197.724-.197.758 0 1.453.606 1.453 1.453z" fill="#111d5a"/><g fill="#83e8c2"><path d="M59.402 90.568zM94.485 123.06zM94.771 123.29zM77.775 122.51zM77.072 123.33zM77.418 123.09zM77.856 122.05zM76.749 123.45zM94.119 122.41zM77.131 133.51l-15.489-8.942v1.993c0 .906-.743 1.555-1.554 1.555a1.53 1.53 0 0 1-.773-.211l-1.726-.996v17.885c0 .519.276.997.726 1.257l17.365 10.026a1.452 1.452 0 0 0 2.177-1.257v-20.053a1.454 1.454 0 0 0-.726-1.257zM94.25 122.74zM110.28 111.42zM94.494 100.98c.088-.089.189-.168.303-.232l17.365-10.026-17.365 10.026a1.392 1.392 0 0 0-.303.232zM77.627 122.83zM58.027 90.936zM58.374 90.693zM59.044 90.521l-.015.001c.083-.001.167.015.251.029-.079-.012-.158-.03-.236-.03zM57.819 91.195zM58.696 90.568zM57.589 91.977zM76.043 123.46zM57.67 91.516zM75.677 123.31l-14.035-8.11zM76.401 123.5l.015-.001c-.082.001-.166-.016-.248-.029.078.012.156.03.233.03zM112.16 90.716zM77.662 101.27zM113.64 90.734zM96.237 123.31zM113.33 90.597zM112.89 90.52c-.075 0-.151.018-.228.029.081-.014.162-.029.242-.028l-.014-.001zM141.26 74.137c-.241 0-.489.062-.724.197l-17.365 10.025c-.449.26-.727.738-.727 1.257v17.885l1.727-.996c.25-.145.515-.211.773-.21.81 0 1.554.647 1.554 1.554v1.993l15.489-8.943a1.45 1.45 0 0 0 .726-1.256V75.59c0-.847-.695-1.453-1.453-1.453zM112.96 90.526zM95.523 123.5c.074 0 .15-.018.225-.029-.08.013-.159.028-.238.028l.013.001zM95.451 123.5zM85.238 86.7zM95.078 123.43zM141.26 106.9c-.241 0-.489.062-.724.196l-14.038 8.105v3.784c0 .555-.296 1.067-.776 1.344l-13.11 7.57c-.251.144-.515.21-.774.21-.81 0-1.553-.648-1.553-1.555v-1.992l-15.488 8.941c-.449.26-.727.738-.727 1.257v20.053a1.452 1.452 0 0 0 2.177 1.257l45.741-26.409a1.45 1.45 0 0 0 .726-1.256v-20.053a1.454 1.454 0 0 0-1.454-1.452zM67.871 41.396a1.451 1.451 0 0 0 0 2.513l14.038 8.104 3.278-1.893c.24-.139.508-.207.775-.207s.536.068.775.207l13.111 7.57a1.552 1.552 0 0 1 0 2.688l-1.727.996 15.49 8.943a1.457 1.457 0 0 0 1.452 0l17.366-10.027a1.45 1.45 0 0 0 0-2.512l-45.741-26.41a1.449 1.449 0 0 0-1.452 0zM39.497 57.779a1.45 1.45 0 0 0 0 2.512l17.366 10.027a1.457 1.457 0 0 0 1.452 0l15.488-8.943-1.727-.996a1.552 1.552 0 0 1 0-2.688l1.727-.997-15.489-8.942a1.458 1.458 0 0 0-1.451 0zM49.481 138.43v-16.209l-3.278-1.894a1.55 1.55 0 0 1-.775-1.344v-15.139c0-.906.743-1.555 1.554-1.554.259 0 .523.065.773.21l1.727.997V85.611a1.45 1.45 0 0 0-.726-1.257L31.39 74.33a1.436 1.436 0 0 0-.724-.197c-.758 0-1.452.606-1.452 1.453v52.817c0 .518.276.997.726 1.256l17.365 10.026a1.45 1.45 0 0 0 2.176-1.255zM114.34 108.18l-3.278 1.893 3.278-1.893V91.971zM114.11 91.193zM114.16 91.283z"/></g><g fill="#de5941"><path d="M94.494 100.98a1.45 1.45 0 0 0-.424 1.023v20.053l.001.002c0 .126.02.244.048.358.01.034.021.066.033.101.026.08.059.156.098.229.017.03.032.061.051.09.055.084.115.162.185.232.009.009.015.02.023.027.079.077.169.142.263.198.027.017.055.029.084.044a1.46 1.46 0 0 0 .596.165c.02.001.039.005.059.006.079 0 .158-.016.238-.028.045-.007.09-.006.135-.018.119-.031.238-.08.354-.146l.01-.004 14.038-8.104v-3.785c0-.18.04-.35.098-.514.122-.343.353-.643.678-.83l3.278-1.893V91.977c0-.127-.021-.246-.049-.361-.009-.033-.021-.065-.032-.099a1.266 1.266 0 0 0-.099-.229c-.017-.031-.032-.061-.05-.09a1.425 1.425 0 0 0-.188-.235l-.021-.024a1.41 1.41 0 0 0-.263-.199c-.027-.016-.056-.029-.084-.043a1.509 1.509 0 0 0-.323-.125 1.591 1.591 0 0 0-.273-.04c-.021-.001-.039-.005-.059-.006-.08-.001-.161.015-.242.028-.043.008-.087.006-.131.018-.123.031-.246.08-.365.149l-17.365 10.026a1.447 1.447 0 0 0-.302.233zM77.13 100.74L59.769 90.717a1.424 1.424 0 0 0-.366-.149c-.041-.012-.082-.01-.122-.017-.084-.015-.168-.03-.251-.029-.019 0-.036.005-.055.005-.095.005-.188.02-.278.041-.034.009-.065.02-.099.03a1.406 1.406 0 0 0-.224.095c-.028.014-.057.027-.084.043a1.515 1.515 0 0 0-.263.199l-.021.025c-.07.071-.133.15-.187.234-.019.029-.034.061-.051.091-.04.073-.072.149-.099.229a1.463 1.463 0 0 0-.081.461v16.206l3.276 1.892a1.547 1.547 0 0 1 .776 1.343v3.784l14.035 8.104c.119.068.242.117.366.149.041.012.082.01.125.017.082.014.166.03.248.029.019 0 .037-.005.055-.006.095-.004.188-.019.278-.041.034-.008.065-.019.099-.029.077-.025.152-.058.225-.095.027-.015.056-.027.082-.043.095-.058.185-.123.264-.199l.021-.025c.07-.071.133-.15.188-.235.018-.029.033-.06.05-.09.04-.072.072-.149.099-.229a1.448 1.448 0 0 0 .081-.461v-20.047a1.456 1.456 0 0 0-.726-1.259zM86.689 86.7l17.365-10.026a1.45 1.45 0 0 0 0-2.513l-14.038-8.105-3.278 1.893a1.556 1.556 0 0 1-1.551 0l-3.277-1.892-14.038 8.104c-.241.14-.423.331-.544.55a1.466 1.466 0 0 0 0 1.413c.121.218.303.41.544.55L85.238 86.7a1.447 1.447 0 0 0 1.451 0z"/></g></g></symbol><symbol viewBox="0 0 24 24" id="riot" xmlns="http://www.w3.org/2000/svg"><defs><path d="M13.26 3.04l.58.05.54.07.52.09.49.11.46.13.44.14.41.16.39.17.36.19.33.21.32.22.29.23.26.25.22.22.2.22.19.24.17.24.15.25.15.26.12.27.12.28.1.29.08.31.07.31.05.32.04.34.02.35.01.37v.05l-.02.51-.05.49-.09.48-.13.45-.15.43-.19.4-.22.39-.26.37-.28.34-.31.33-.33.3-.37.28-.39.27-.41.24-.44.22L21 21h-7.04l-3.48-5.14H9.17V21H3V3h9.01l.64.01.61.03zm-4.09 8.52h2.66l.99-.11.75-.35.47-.55.16-.74v-.05l-.17-.75-.47-.54-.74-.32-.96-.11H9.17v3.52z" id="ija"/></defs><use xlink:href="#ija" fill="#ff1744"/><use xlink:href="#ija" fill-opacity="0" stroke="#000" stroke-opacity="0"/></symbol><symbol viewBox="0 0 24 24" id="robot" xmlns="http://www.w3.org/2000/svg"><path d="M12.05 2.804a1.787 1.787 0 0 1 1.788 1.788c0 .661-.357 1.242-.893 1.546v1.135h.893a6.256 6.256 0 0 1 6.256 6.256h.894a.894.894 0 0 1 .893.893v2.681a.894.894 0 0 1-.893.894h-.894v.894a1.787 1.787 0 0 1-1.787 1.787H5.795a1.787 1.787 0 0 1-1.787-1.787v-.894h-.894a.894.894 0 0 1-.894-.894v-2.68a.894.894 0 0 1 .894-.894h.894a6.256 6.256 0 0 1 6.255-6.256h.894V6.138a1.773 1.773 0 0 1-.894-1.546 1.787 1.787 0 0 1 1.788-1.788m-4.022 9.83a2.234 2.234 0 0 0-2.234 2.235 2.234 2.234 0 0 0 2.234 2.234 2.234 2.234 0 0 0 2.234-2.234 2.234 2.234 0 0 0-2.234-2.234m8.043 0a2.234 2.234 0 0 0-2.234 2.234 2.234 2.234 0 0 0 2.234 2.234 2.234 2.234 0 0 0 2.235-2.234 2.234 2.234 0 0 0-2.235-2.234z" fill="#ff5722" stroke-width=".894"/></symbol><symbol viewBox="100 100 800 800" id="rollup" xmlns="http://www.w3.org/2000/svg"><style>.ilst0{fill:url(#ilXMLID_4_)}.ilst1{fill:url(#ilXMLID_5_)}.ilst2{fill:url(#ilXMLID_8_)}.ilst3{fill:url(#ilXMLID_9_)}.ilst4{fill:url(#ilXMLID_11_)}.ilst5{opacity:.3;fill:url(#ilXMLID_16_)}</style><g id="ilXMLID_14_" transform="translate(-54.117 -62.353) scale(1.1129)"><linearGradient id="ilXMLID_4_" x1="444.47" x2="598.47" y1="526.05" y2="562.05" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6533" offset="0"/><stop stop-color="#FF5633" offset=".157"/><stop stop-color="#FF4333" offset=".434"/><stop stop-color="#FF3733" offset=".714"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_15_" class="ilst0" d="M721 410c0-33.6-8.8-65.1-24.3-92.4-41.1-42.3-130.5-52.1-152.7-.2-22.8 53.2 38.3 112.4 65 107.7 34-6-6-84-6-84 52 98 40 68-54 158S359 779 345 787c-.6.4-1.2.7-1.9 1h368.7c6.5 0 10.7-6.9 7.8-12.7l-96.4-190.8c-2.1-4.1-.6-9.2 3.4-11.5C683 540.6 721 479.8 721 410z" fill="url(#ilXMLID_4_)"/></g><g id="ilXMLID_2_" transform="translate(-54.117 -62.353) scale(1.1129)"><linearGradient id="ilXMLID_5_" x1="420.38" x2="696.38" y1="475" y2="689" gradientUnits="userSpaceOnUse"><stop stop-color="#BF3338" offset="0"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_10_" class="ilst1" d="M721 410c0-33.6-8.8-65.1-24.3-92.4-41.1-42.3-130.5-52.1-152.7-.2-22.8 53.2 38.3 112.4 65 107.7 34-6-6-84-6-84 52 98 40 68-54 158S359 779 345 787c-.6.4-1.2.7-1.9 1h368.7c6.5 0 10.7-6.9 7.8-12.7l-96.4-190.8c-2.1-4.1-.6-9.2 3.4-11.5C683 540.6 721 479.8 721 410z" fill="url(#ilXMLID_5_)"/></g><linearGradient id="ilXMLID_8_" x1="429.39" x2="469.39" y1="517.16" y2="559.16" gradientTransform="translate(-54.117 -62.353) scale(1.1129)" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6533" offset="0"/><stop stop-color="#FF5633" offset=".157"/><stop stop-color="#FF4333" offset=".434"/><stop stop-color="#FF3733" offset=".714"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_3_" class="ilst2" d="M329.82 813.46c15.58-8.903 122.41-220.34 227.02-320.5s117.96-66.771 60.094-175.83c0 0-221.46 310.49-301.58 464.06" fill="url(#ilXMLID_8_)" stroke-width="1.113"/><g id="ilXMLID_7_" transform="translate(-54.117 -62.353) scale(1.1129)"><linearGradient id="ilXMLID_9_" x1="502.11" x2="490.11" y1="589.46" y2="417.46" gradientUnits="userSpaceOnUse"><stop stop-color="#FF6533" offset="0"/><stop stop-color="#FF5633" offset=".157"/><stop stop-color="#FF4333" offset=".434"/><stop stop-color="#FF3733" offset=".714"/><stop stop-color="#F33" offset="1"/></linearGradient><path id="ilXMLID_12_" class="ilst3" d="M373 537c134.4-247.1 152-272 222-272 36.8 0 73.9 16.6 97.9 46.1-32.7-52.7-90.6-88-156.9-89H307.7c-4.8 0-8.7 3.9-8.7 8.7V691c13.6-35.1 36.7-85.3 74-154z" fill="url(#ilXMLID_9_)"/></g><linearGradient id="ilXMLID_11_" x1="450.12" x2="506.94" y1="514.21" y2="552.85" gradientTransform="translate(-54.117 -62.353) scale(1.1129)" gradientUnits="userSpaceOnUse"><stop stop-color="#FBB040" offset="0"/><stop stop-color="#FB8840" offset="1"/></linearGradient><path id="ilXMLID_6_" class="ilst4" d="M556.84 492.96c-104.61 100.16-211.44 311.6-227.02 320.5s-41.732 10.016-55.643-5.564c-14.801-16.582-37.837-43.401 86.802-272.65 149.57-274.99 169.15-302.7 247.05-302.7 40.953 0 82.24 18.473 108.95 51.302 1.447 2.337 2.893 4.785 4.34 7.233-45.738-47.074-145.23-57.98-169.93-.222-25.373 59.204 42.622 125.08 72.335 119.85 37.837-6.677-6.677-93.48-6.677-93.48 57.757 108.95 44.403 75.563-60.205 175.72z" fill="url(#ilXMLID_11_)" stroke-width="1.113"/><linearGradient id="ilXMLID_16_" x1="508.33" x2="450.33" y1="295.76" y2="933.76" gradientTransform="translate(-54.117 -62.353) scale(1.1129)" gradientUnits="userSpaceOnUse"><stop stop-color="#FFF" offset="0"/><stop stop-color="#FFF" stop-opacity="0" offset="1"/></linearGradient><path id="ilXMLID_13_" class="ilst5" d="M373.22 547.49c149.57-274.99 169.15-302.7 247.05-302.7 33.719 0 67.661 12.575 93.48 35.277-26.708-30.492-66.326-47.519-105.72-47.519-77.9 0-97.486 27.71-247.05 302.7-124.64 229.25-101.6 256.07-86.802 272.65 2.114 2.337 4.563 4.34 7.122 6.01-13.02-18.919-18.807-62.877 91.922-266.42z" fill="url(#ilXMLID_16_)" opacity=".3" stroke-width="1.113"/></symbol><symbol viewBox="0 0 24 24" id="ruby" xmlns="http://www.w3.org/2000/svg"><path d="M16 9h3l-5 7m-4-7h4l-2 8M5 9h3l2 7m5-12h2l2 3h-3m-5-3h2l1 3h-4M7 4h2L8 7H5m1-5L2 8l10 14L22 8l-4-6H6z" fill="#f44336"/></symbol><symbol viewBox="0 0 144 144" id="rust" xmlns="http://www.w3.org/2000/svg"><path d="M68.252 26.206a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0M25.766 58.451a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m84.97.166a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m-74.661 4.88a3.252 3.252 0 0 0 1.651-4.29l-1.58-3.574h6.214v28.01H29.823a43.847 43.847 0 0 1-1.42-16.738zm25.994.688v-8.256h14.798c.764 0 5.397.883 5.397 4.347 0 2.877-3.553 3.908-6.475 3.908zm-20.203 44.452a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m52.769.166a3.561 3.561 0 0 1 7.123 0 3.561 3.561 0 0 1-7.123 0m1.101-8.076a3.246 3.246 0 0 0-3.856 2.498l-1.787 8.342a43.847 43.847 0 0 1-36.566-.175l-1.787-8.342a3.246 3.246 0 0 0-3.854-2.497l-7.365 1.581a43.847 43.847 0 0 1-3.808-4.488h35.834c.406 0 .676-.074.676-.443V84.527c0-.369-.27-.442-.676-.442h-10.48V76.05h11.335c1.035 0 5.532.296 6.97 6.045.45 1.768 1.44 7.519 2.116 9.36.674 2.065 3.417 6.19 6.34 6.19h18.501a43.847 43.847 0 0 1-4.06 4.7zm19.898-33.468a43.847 43.847 0 0 1 .093 7.612h-4.499c-.45 0-.631.296-.631.737v2.066c0 4.863-2.742 5.92-5.145 6.19-2.288.258-4.825-.958-5.138-2.358-1.35-7.593-3.6-9.214-7.152-12.016 4.409-2.8 8.996-6.93 8.996-12.457 0-5.97-4.092-9.729-6.881-11.572-3.914-2.58-8.246-3.096-9.415-3.096H39.336A43.847 43.847 0 0 1 63.867 28.52l5.484 5.753a3.243 3.243 0 0 0 4.59.105l6.137-5.869a43.847 43.847 0 0 1 30.017 21.38l-4.201 9.487a3.256 3.256 0 0 0 1.652 4.29zm10.477.154l-.143-1.467 4.327-4.036c.88-.82.55-2.472-.574-2.891l-5.532-2.068-.433-1.428 3.45-4.792c.704-.974.058-2.53-1.127-2.724l-5.833-.949-.7-1.31 2.45-5.38c.502-1.095-.43-2.496-1.636-2.45l-5.92.206-.935-1.135 1.36-5.766c.275-1.17-.913-2.36-2.084-2.085l-5.765 1.359-1.136-.935.207-5.92c.046-1.198-1.357-2.135-2.45-1.637l-5.379 2.452-1.31-.703-.95-5.833c-.193-1.183-1.75-1.83-2.723-1.128l-4.796 3.45-1.425-.432-2.068-5.532c-.42-1.127-2.072-1.452-2.89-.576l-4.036 4.33-1.467-.143-3.117-5.036c-.63-1.02-2.318-1.02-2.946 0l-3.117 5.036-1.467.143-4.037-4.33c-.819-.876-2.47-.551-2.89.576l-2.069 5.532-1.426.432-4.795-3.45c-.974-.703-2.53-.055-2.723 1.128l-.951 5.833-1.31.703-5.379-2.452c-1.093-.5-2.496.439-2.45 1.637l.206 5.92-1.136.935-5.765-1.36c-1.171-.272-2.36.915-2.086 2.086l1.358 5.766-.933 1.135-5.92-.206c-1.193-.035-2.134 1.355-1.637 2.45l2.453 5.38-.703 1.31-5.832.949c-1.185.192-1.827 1.75-1.128 2.724l3.45 4.792-.433 1.428-5.532 2.068c-1.123.42-1.452 2.07-.574 2.891l4.328 4.036-.143 1.467-5.035 3.116c-1.02.63-1.02 2.318 0 2.946l5.035 3.117.143 1.467-4.328 4.037c-.878.818-.549 2.468.574 2.89l5.532 2.068.433 1.428-3.45 4.793c-.701.976-.056 2.532 1.129 2.723l5.831.948.703 1.312-2.453 5.378c-.5 1.093.444 2.5 1.638 2.451l5.917-.207.935 1.136-1.358 5.768c-.275 1.168.915 2.355 2.086 2.08l5.765-1.357 1.137.932-.207 5.921c-.046 1.199 1.357 2.136 2.45 1.636l5.379-2.45 1.31.702.95 5.83c.193 1.187 1.75 1.829 2.725 1.13l4.792-3.453 1.427.435 2.069 5.53c.42 1.123 2.072 1.454 2.89.574l4.037-4.328 1.467.146 3.117 5.035c.628 1.016 2.316 1.018 2.946 0l3.117-5.035 1.467-.146 4.036 4.328c.818.88 2.47.549 2.89-.574l2.068-5.53 1.428-.435 4.793 3.453c.974.699 2.53.055 2.722-1.13l.952-5.83 1.31-.703 5.378 2.451c1.093.5 2.493-.435 2.45-1.636l-.206-5.92 1.135-.933 5.765 1.357c1.171.275 2.36-.912 2.085-2.08l-1.358-5.768.932-1.136 5.92.207c1.194.048 2.138-1.358 1.636-2.451l-2.45-5.378.7-1.312 5.833-.948c1.187-.19 1.831-1.747 1.127-2.723l-3.45-4.793.433-1.428 5.532-2.068c1.125-.422 1.454-2.072.574-2.89l-4.327-4.037.143-1.467 5.035-3.117c1.02-.628 1.021-2.315.001-2.946z" fill="#ff7043" stroke-width="1.146"/></symbol><symbol viewBox="0 0 500 500" id="sass" xmlns="http://www.w3.org/2000/svg"><path d="M422.676 96.573c-12.192-47.839-91.508-63.557-166.575-36.892-44.68 15.877-93.029 40.786-127.81 73.311-41.349 38.675-47.943 72.328-45.216 86.395 9.583 49.622 77.585 82.069 105.535 106.126v.144c-8.246 4.05-68.565 34.584-82.684 65.799-14.893 32.932 2.372 56.556 13.804 59.742 35.424 9.859 71.764-7.866 91.311-37.01 18.853-28.12 17.28-64.422 9.086-82.487 11.3-2.976 24.476-4.314 41.218-2.36 47.248 5.52 56.517 35.017 54.747 47.366-1.77 12.35-11.681 19.14-14.998 21.186-3.317 2.045-4.326 2.766-4.05 4.287.405 2.215 1.94 2.137 4.758 1.652 3.894-.656 24.804-10.042 25.709-32.828 1.14-28.933-26.587-61.302-75.684-60.45-20.216.354-32.933 2.268-42.123 5.69-.681-.774-1.363-1.547-2.084-2.307-30.35-32.382-86.46-55.285-84.088-98.824.866-15.823 6.372-57.5 107.817-108.052 83.104-41.415 149.637-30.009 161.135-4.76 16.427 36.08-35.554 103.137-121.858 112.812-32.88 3.684-50.198-9.059-54.498-13.804-4.536-4.995-5.204-5.218-6.909-4.287-2.753 1.533-1.01 5.938 0 8.574 2.583 6.712 13.15 18.603 31.176 24.515 15.863 5.205 54.459 8.063 101.156-9.99 52.283-20.255 93.12-76.523 81.125-123.548zM200.213 340.34c3.92 14.5 3.487 28.016-.564 40.248a65.289 65.289 0 0 1-3.225 7.97c-3.12 6.477-7.316 12.534-12.442 18.132-15.653 17.069-37.507 23.532-46.88 18.092-10.122-5.874-5.048-29.944 13.083-49.11 19.52-20.636 47.602-33.903 47.602-33.903l-.039-.079 2.465-1.35z" fill="#ec407a" stroke="#ec407a" stroke-width="16.286552999999998"/></symbol><symbol viewBox="0 0 300 300" id="sbt" xmlns="http://www.w3.org/2000/svg"><path d="M105.46 209.517c-7.875 0-13.452-7.521-13.452-15.37v-.327c0-7.848 5.578-13.735 13.452-13.735h164.05c1.476-4.905 2.625-11.446 3.281-17.986h-137.81c-7.875 0-14.273-6.05-14.273-13.898s6.398-13.898 14.273-13.898h137.31c-.82-6.54-1.969-13.081-3.773-17.986h-104.01c-7.875 0-14.273-6.05-14.273-13.898s6.398-13.898 14.273-13.898h91.87c-21.327-37.607-60.864-61.315-106.14-61.315-67.918 0-123.04 54.448-123.04 122.3 0 67.856 55.122 123.28 123.04 123.28 46.59 0 87.112-25.507 107.95-63.114h-152.73z" fill="#0277bd" stroke-width="1.638"/></symbol><symbol viewBox="0 0 256 256" id="scala" xmlns="http://www.w3.org/2000/svg"><path fill="#f44336" fill-rule="evenodd" stroke-width=".3" d="M59.607 50.647l149.097-21.982v49.488L59.607 100.135zM59.593 114.08L208.69 92.098v49.488L59.593 163.568zM59.587 177.358l149.097-21.982v49.488L59.587 226.846z"/><path fill="#f44336" fill-rule="evenodd" stroke-width=".3" d="M62.425 91.414l95.605 30.923-2.832 8.757-95.605-30.922zM113.084 61.13l95.604 30.922-2.832 8.757-95.605-30.922zM62.425 154.79l95.605 30.922-2.833 8.758-95.604-30.923zM113.097 124.408l95.604 30.923-2.832 8.757-95.605-30.922z"/></symbol><symbol viewBox="0 0 24 24" id="settings" xmlns="http://www.w3.org/2000/svg"><path d="M12 15.5A3.5 3.5 0 0 1 8.5 12 3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97 0-.33-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1 0 .33.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.66z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="shaderlab" xmlns="http://www.w3.org/2000/svg"><path d="M9.11 17H6.5l-4.91-5L6.5 7h2.61l1.31-2.26L17.21 3l1.87 6.74L17.77 12l1.31 2.26L17.21 21l-6.79-1.74L9.11 17m.14-.25l5.13 1.38L11.42 13H5.5l3.75 3.75m6.87.38L17.5 12l-1.38-5.13L13.15 12l2.97 5.13M9.25 7.25L5.5 11h5.92l2.96-5.13-5.13 1.38z" fill="#1976d2"/></symbol><symbol viewBox="0 0 24 24" id="slim" xmlns="http://www.w3.org/2000/svg"><path d="M6.959 2.5a4.605 4.605 0 0 0-4.615 4.615v9.957a4.605 4.605 0 0 0 4.615 4.615h9.957a4.605 4.605 0 0 0 4.615-4.615V7.115A4.605 4.605 0 0 0 16.916 2.5zm4.938 2.691a6.811 6.811 0 0 1 6.81 6.813H13.43L9.938 7.287l.699 4.717H5.086a6.811 6.811 0 0 1 6.81-6.813z" fill="#f57f17"/></symbol><symbol id="smarty" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.iust0{fill:#ffce00}</style><path class="iust0" d="M9.14 20.606c0 .556.398.953.954.953h3.812c.556 0 .953-.397.953-.953v-.953H9.141zM12 2.5c-3.653 0-6.671 3.018-6.671 6.671 0 2.303 1.112 4.289 2.859 5.48v2.144c0 .556.397.953.953.953h5.718c.556 0 .953-.397.953-.953V14.65c1.747-1.191 2.86-3.177 2.86-5.48 0-3.653-3.019-6.671-6.672-6.671zm2.7 10.563l-.794.555v2.224h-3.812v-2.224l-.794-.555A4.712 4.712 0 0 1 7.235 9.17 4.78 4.78 0 0 1 12 4.405a4.78 4.78 0 0 1 4.765 4.765 4.712 4.712 0 0 1-2.065 3.892z"/></symbol><symbol viewBox="0 0 200 200" id="snyk" xmlns="http://www.w3.org/2000/svg"><title>Group 2</title><g transform="translate(15.255 18.22) scale(1.8477)" fill="none" fill-rule="evenodd"><path d="M65.161 24.997c-1.656 5.974-5.255 23.587-5.255 23.587s-6.618-2.464-14.148-2.476h-.055c-.413.002-.822.012-1.23.026v41.649h6.677v.003h5.815v-.003h20.858c.111-8.177-2.036-27.066-2.036-27.066-1.088-2.279.46-7.668.46-7.668-8.869-9.092-11.086-28.051-11.086-28.051zm-3.357 43.958c5.476 0 1.381 4.64.9 5.168H52.35c.944-1.18 4.504-5.168 9.453-5.168z" fill="#607d8b" stroke-width="1.6"/><path d="M26.366 24.995s-2.217 18.961-11.087 28.053c0 0 1.548 5.391.46 7.669 0 0-2.15 18.895-2.038 27.066h19.273v.003h7.079v-.003h5.744V46.107h-.025c-7.532.013-14.151 2.478-14.151 2.478s-3.6-17.615-5.255-23.59zm3.264 43.96c4.95 0 8.51 3.987 9.452 5.168H28.73c-.479-.528-4.573-5.168.9-5.168z" fill="#90a4ae" stroke-width="1.6"/><g transform="translate(23.76 77.45) scale(1.5998)"><g transform="translate(17.526)"><path d="M7.357.06H.177v.075C.177 2.64 2.345 4.67 4.89 4.67 7.431 4.67 9.6 2.64 9.6.135V.059z" fill="#455a64"/><path d="M1.972.06v.075a2.692 2.692 0 1 0 5.386 0V.059z" fill="#fff"/><path d="M5.496.06H4.234c-.012 0-.023.005-.034.007.157.033.243.388.21.624a.721.721 0 0 1-.71.617c.102.471.487.85.997.922a1.188 1.188 0 0 0 1.35-1.007C6.112.743 5.881.06 5.495.06z" fill="#37474f"/></g><path d="M7.552.06H.372v.075c0 2.505 2.17 4.535 4.712 4.535 2.544 0 4.712-2.03 4.712-4.535V.059z" fill="#455a64"/><path d="M2.168.06v.075a2.692 2.692 0 1 0 5.385 0V.059z" fill="#fff"/><path d="M5.692.06H4.428c-.01 0-.022.005-.032.007.156.033.242.388.21.624a.72.72 0 0 1-.712.617c.104.471.488.85.999.922A1.187 1.187 0 0 0 6.24 1.223C6.308.743 6.078.06 5.69.06z" fill="#37474f"/></g><path d="M25.514-.27l-4.202 7.697C19.838 10.17 6.858 34.465 6.858 43.243v.516L12.8 59.573c-.8 7.258-2.203 21.643-1.78 28.21h5.73c-.354-3.787.648-17.008 1.903-28.25l.076-.677-1.075-2.892c3.694-3.868 6.285-9.193 8.073-14.261l.174 1.235 5.869 9.629 2.291-.983c.058-.024 5.935-2.523 11.643-2.523 5.672 0 11.646 2.5 11.702 2.525l2.29.976 5.86-9.626.23-1.608c1.769 5.117 4.358 10.536 8.07 14.49l-1.127 3.035.076.678c1.259 11.286 2.266 24.564 1.916 28.252h5.677c.406-6.567-1.05-20.952-1.848-28.208l5.838-15.817v-.514c0-8.779-12.876-33.074-14.347-35.816L65.923-.27l-5.897 41.229-2.723 4.478c-2.628-.882-7.1-2.11-11.603-2.11-4.498 0-8.94 1.225-11.557 2.108l-2.722-4.476-2.07-14.452a.832.832 0 0 0 .006-.071l-.016-.004zm-3.166 18.39l1.206 8.407c-.46 3.143-2.561 15.47-8.198 23.24l-2.598-6.99c.325-4.554 5.067-15.462 9.59-24.656zm46.763 0c4.523 9.194 9.267 20.104 9.592 24.657L76.166 49.6c-6.09-8.553-8-22.459-8.166-23.73z" fill="#607d8b" stroke-width="1.6"/></g></symbol><symbol viewBox="0 0 24 24" id="solidity" xmlns="http://www.w3.org/2000/svg"><path d="M5.8 14.05l6.253 8.61 6.252-8.61-6.254 3.807z" fill="#0288d1" stroke-width="4.553" stroke-linejoin="round"/><path d="M12.051 1.347L5.8 11.833l6.252 3.807 6.254-3.807z" fill="#0288d1" stroke-width="5.025" stroke-linejoin="round"/></symbol><symbol viewBox="0 0 120 120" id="sonar" xmlns="http://www.w3.org/2000/svg"><style>.a,.b{fill:#fff}.b{stroke:#fff;stroke-miterlimit:10}</style><path d="M115.45 23.033S97.961 33.27 97.534 33.412c-.427.284-.852.57-1.137.854-1.422 1.421-1.848 3.41-1.422 5.26.285.852.711 1.849 1.422 2.56.711.71 1.564 1.137 2.559 1.422 1.848.426 3.84 0 5.262-1.422.426-.427.709-.853.851-1.28l.143-.427 2.56-4.692zm-39.102 9.242c-27.441 0-31.99 13.08-31.99 29.29 0 3.838.569 7.962-1.99 11.942-3.84 5.972-8.957 5.828-10.236 5.828-1.706 0-7.962-.993-8.246-2.841h.994c6.682 0 11.658-5.404 11.658-12.655v-2.56h-5.686c-4.123 0-7.82 1.849-10.238 5.12-2.417-3.271-6.113-5.12-10.236-5.12h-5.83v2.56c0 7.11 5.688 12.795 12.797 12.795h1.848c0 4.124 5.687 20.332 47.63 20.332 16.352 0 40.665-2.843 40.665-33.697 0-5.829-1.848-11.23-4.691-15.78-.996.284-1.992.568-3.13.568a8.92 8.92 0 0 1-8.956-8.957c0-.995.141-1.991.425-2.986-4.265-2.702-8.53-3.838-14.787-3.838z" fill="#1e88e5" stroke-width="1.422"/></symbol><symbol viewBox="0 0 412 395" id="stylelint" xmlns="http://www.w3.org/2000/svg"><title>stylelint-icon-white</title><g transform="translate(31.478 29.499) scale(.84775)" fill="#cfd8dc" fill-rule="evenodd"><path d="M208.8 393.05c45.057-161.12 43.75-161.85 76.32-276.73l7.832 4.523c4.255 2.458 7.738.448 7.738-4.455V61.602c8.643-30.27 15.416-53.66 17.4-60.693h35.287l58.618 54.304-38.498 33.27 29.11 31.473-191.86 273.09c-.938 1.542-2.244 1.19-1.947 0zm20.96-347.28c1.733 0 3.148.958 3.148 2.147v28.077c0 1.186-1.415 2.15-3.147 2.15h-47.396c-1.742 0-3.153-.96-3.153-2.15V47.917c0-1.185 1.41-2.147 3.153-2.147h47.396z"/><path d="M288.26 14.688l-52.14 30.1c.605.92.973 1.98.973 3.136v28.078c0 1.457-.565 2.77-1.496 3.83l52.663 30.402c3.59 2.073 6.535.377 6.535-3.764V18.456c0-4.145-2.944-5.836-6.535-3.768zM175.02 76V47.923c0-1.15.368-2.21.966-3.13l-52.14-30.105c-3.588-2.068-6.53-.376-6.53 3.768v88.013c0 4.14 2.938 5.84 6.53 3.76l52.66-30.405c-.926-1.06-1.487-2.37-1.487-3.827z"/><path d="M201.25 393.05h1.947c-45.05-161.12-43.753-161.85-76.32-276.73l-7.833 4.523c-4.253 2.458-7.737.448-7.737-4.455V61.602C102.662 31.332 95.892 7.942 93.902.909H58.619L.002 55.213l38.494 33.27-29.11 31.473z"/><circle cx="204.57" cy="122.54" r="14.231"/><circle cx="204.57" cy="207.16" r="14.231"/><circle cx="204.57" cy="291.78" r="14.23"/></g></symbol><symbol viewBox="0 0 412 395" id="stylelint_light" xmlns="http://www.w3.org/2000/svg"><title>stylelint-icon-black</title><g transform="translate(31.478 29.499) scale(.84775)" fill="#546e7a" fill-rule="evenodd"><path d="M208.8 393.05c45.057-161.12 43.75-161.85 76.32-276.73l7.832 4.523c4.255 2.458 7.738.448 7.738-4.455V61.602c8.643-30.27 15.416-53.66 17.4-60.693h35.287l58.618 54.304-38.498 33.27 29.11 31.473-191.86 273.09c-.938 1.542-2.244 1.19-1.947 0zm20.96-347.28c1.733 0 3.148.958 3.148 2.147v28.077c0 1.186-1.415 2.15-3.147 2.15h-47.396c-1.742 0-3.153-.96-3.153-2.15V47.917c0-1.185 1.41-2.147 3.153-2.147h47.396z"/><path d="M288.26 14.688l-52.14 30.1c.605.92.973 1.98.973 3.136v28.078c0 1.457-.565 2.77-1.496 3.83l52.663 30.402c3.59 2.073 6.535.377 6.535-3.764V18.456c0-4.145-2.944-5.836-6.535-3.768zM175.02 76V47.923c0-1.15.368-2.21.966-3.13l-52.14-30.105c-3.588-2.068-6.53-.376-6.53 3.768v88.013c0 4.14 2.938 5.84 6.53 3.76l52.66-30.405c-.926-1.06-1.487-2.37-1.487-3.827z"/><path d="M201.25 393.05h1.947c-45.05-161.12-43.753-161.85-76.32-276.73l-7.833 4.523c-4.253 2.458-7.737.448-7.737-4.455V61.602C102.662 31.332 95.892 7.942 93.902.909H58.619L.002 55.213l38.494 33.27-29.11 31.473z"/><circle cx="204.57" cy="122.54" r="14.231"/><circle cx="204.57" cy="207.16" r="14.231"/><circle cx="204.57" cy="291.78" r="14.23"/></g></symbol><symbol viewBox="0 0 200.00001 200.00001" id="stylus" xmlns="http://www.w3.org/2000/svg"><path d="M126.814 155.9c14.64-17.51 16.362-35.595 5.024-69.18-7.177-21.24-19.09-37.602-10.334-50.807 9.329-14.065 29.135-.43 12.63 18.371l3.301 2.297c19.806 2.296 29.566-24.83 14.783-32.58C113.179 3.621 79.02 42.803 94.09 88.156c6.458 19.232 15.5 39.613 8.18 55.83-6.314 13.923-18.514 22.103-26.695 22.39-17.079.862-5.74-38.32 13.922-48.08 1.722-.861 4.162-2.01 1.866-4.88-24.256-2.727-38.464 8.468-46.645 24.112-23.825 45.497 45.21 62.29 82.095 18.371z" fill="#c0ca33" stroke-width="1.435"/></symbol><symbol viewBox="0 0 24 24" id="swc" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="jba"><stop offset="0" stop-color="#791223"/><stop offset="1" stop-color="#d92f3c"/></linearGradient><linearGradient xlink:href="#jba" id="jbb" x1="12.356" y1="21.559" x2="12.356" y2="2.949" gradientUnits="userSpaceOnUse"/></defs><path d="M6 3c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6 3 6.5V19a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6.5c0-.5-.17-.93-.46-1.27l-1.39-1.68C18.88 3.21 18.47 3 18 3H6zm-.07 1h12l.94 1H5.12l.81-1z" fill="url(#jbb)"/><path style="line-height:125%" d="M11.053 11.918h-.008c-.244.022-.475.054-.676.11a2.9 2.9 0 0 0-.856.412 3.399 3.399 0 0 0-.67.683 9.36 9.36 0 0 0-.586.95c-.07.131-.134.244-.201.365v.001h-.002l-.768 1.372-.003-.001c-.136.253-.264.485-.38.686-.123.212-.26.39-.411.539a1.599 1.599 0 0 1-.52.34c-.04.016-.092.024-.138.036h-.567v1.383H5.834v-.001c.245-.02.477-.053.679-.11a2.9 2.9 0 0 0 .856-.411c.245-.185.469-.413.67-.683.195-.275.39-.591.585-.95.07-.131.135-.244.202-.366l.004.001.002-.002.02-.038H10.948v-1.378h-.19v-.001H9.624c.125-.234.246-.452.355-.64.123-.21.259-.39.41-.538.152-.148.325-.26.52-.34.04-.015.091-.024.136-.035h.57V13.3h-.002v-1.381h-.56v-.001z" font-weight="400" font-size="40" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#fff"/></symbol><symbol viewBox="0 0 24 24" id="swift" xmlns="http://www.w3.org/2000/svg"><path d="M17.09 19.72c-2.36 1.36-5.59 1.5-8.86.1A13.807 13.807 0 0 1 2 14.5c.67.55 1.46 1 2.3 1.4 3.37 1.57 6.73 1.46 9.1 0-3.37-2.59-6.24-5.96-8.37-8.71-.45-.45-.78-1.01-1.12-1.51 8.28 6.05 7.92 7.59 2.41-1.01 4.89 4.94 9.43 7.74 9.43 7.74.16.09.25.16.36.22.1-.25.19-.51.26-.78.79-2.85-.11-6.12-2.08-8.81 4.55 2.75 7.25 7.91 6.12 12.24-.03.11-.06.22-.05.39 2.24 2.83 1.64 5.78 1.35 5.22-1.21-2.39-3.48-1.65-4.62-1.17z" fill="#fe5e2f"/></symbol><symbol viewBox="0 0 24 24" id="table" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m7 1.5V9h5.5L13 3.5m4 7.5h-4v2h1l-2 1.67L10 13h1v-2H7v2h1l3 2.5L8 18H7v2h4v-2h-1l2-1.67L14 18h-1v2h4v-2h-1l-3-2.5 3-2.5h1v-2z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 200 200" id="terraform" xmlns="http://www.w3.org/2000/svg"><g transform="translate(177.03 -58.705) scale(.92881)" fill="#5c6bc0" stroke="#b0aff5" stroke-linejoin="round"><g stroke-width=".288"><path transform="skewY(26.439) scale(.89541 1)" d="M-203.8 170.95h64.714v51.88H-203.8zM-124.37 171.04h64.714v51.88h-64.714zM-124.37 236.09h64.714v51.88h-64.714z"/></g><path transform="skewY(-22.59) scale(-.92328 1)" stroke-width=".284" d="M-19.172 128.27h62.76v51.88h-62.76z"/></g></symbol><symbol viewBox="0 0 24 24" id="test-js" xmlns="http://www.w3.org/2000/svg"><path d="M5 19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57L13 8.35V4h-2v4.35L5.18 18.43c-.11.16-.18.36-.18.57m1 3a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3H6m7-6l1.34-1.34L16.27 18H7.73l2.66-4.61L13 16m-.5-4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#ffca28"/></symbol><symbol viewBox="0 0 24 24" id="test-jsx" xmlns="http://www.w3.org/2000/svg"><path d="M5 19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57L13 8.35V4h-2v4.35L5.18 18.43c-.11.16-.18.36-.18.57m1 3a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3H6m7-6l1.34-1.34L16.27 18H7.73l2.66-4.61L13 16m-.5-4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#00bcd4"/></symbol><symbol viewBox="0 0 24 24" id="test-ts" xmlns="http://www.w3.org/2000/svg"><path d="M5 19a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1c0-.21-.07-.41-.18-.57L13 8.35V4h-2v4.35L5.18 18.43c-.11.16-.18.36-.18.57m1 3a3 3 0 0 1-3-3c0-.6.18-1.16.5-1.63L9 7.81V6a1 1 0 0 1-1-1V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1v1.81l5.5 9.56c.32.47.5 1.03.5 1.63a3 3 0 0 1-3 3H6m7-6l1.34-1.34L16.27 18H7.73l2.66-4.61L13 16m-.5-4a.5.5 0 0 1 .5.5.5.5 0 0 1-.5.5.5.5 0 0 1-.5-.5.5.5 0 0 1 .5-.5z" fill="#0288d1"/></symbol><symbol viewBox="0 0 500 500" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="tex" xmlns="http://www.w3.org/2000/svg"><g font-weight="400" font-size="40" font-family="sans-serif" letter-spacing="0" word-spacing="0" fill="#42a5f5" stroke-linejoin="miter"><text style="line-height:125%" x="9.914" y="364.919"><tspan x="9.914" y="364.919" font-size="287.5">T</tspan></text><text style="line-height:125%" x="136.374" y="435.558"><tspan x="136.374" y="435.558" font-size="287.5">E</tspan></text><text style="line-height:125%" x="307.819" y="361.201"><tspan x="307.819" y="361.201" font-size="287.5">X</tspan></text></g></symbol><symbol viewBox="0 0 24 24" id="todo" xmlns="http://www.w3.org/2000/svg"><path d="M3 5h6v6H3V5m2 2v2h2V7H5m6 0h10v2H11V7m0 8h10v2H11v-2m-6 5l-3.5-3.5 1.41-1.41L5 17.17l4.59-4.58L11 14l-6 6z" fill="#42a5f5"/></symbol><symbol id="travis" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><style id="jkstyle2">.jkst0{fill:#cb3349}.jkst1{fill:#f4edae}.jkst2{fill:#e6ccad}.jkst3{fill:#656c67}.jkst4{fill:#e5caa3}.jkst5{fill:#c7b39a}.jkst6{fill:#ebd599}.jkst7{fill:#2d3136}.jkst8{fill:#edf6fa}.jkst9{opacity:.8}.jkst10{opacity:.75;fill:#ebd599}</style><g id="jkg99" transform="translate(11.017 12.484) scale(.8858)"><g id="jkg10"><path class="jkst0" d="M47.781 86.572s-31.118 21.903-32.335 30.247l2.335-.48S55.045 91.64 84.584 88.628l.669-3.749z" id="jkpath4" fill="#cb3349"/><path class="jkst0" d="M96.629 83.442l-24.511 17.385 1.325 1.063c.999-.806 43.539-13.798 43.539-13.798l8.969-5.623c-6.018.749-29.322.973-29.322.973z" id="jkpath6" fill="#cb3349"/><path class="jkst0" d="M117.932 104.469c17.405 0 43.495-17.046 43.495-17.046l-8.434-1.605c-.417.417-13.6-.462-13.6-.462l-6.258-1.738-14.951 17.036-1.217 2.956c1.075-.437.965.859.965.859z" id="jkpath8" fill="#cb3349"/></g><path class="jkst0" d="M174.728 158.832l-5.377 1.514-24.843-.537-15.541-12.085-18.784 4.7-21.726-1.88-12.166 13.294-22.828 6.819-11.398-3.534-.574-.494 5.116 12.527s11.588 12.424 18.061 13.885c6.472 1.461 18.165-.105 26.935-1.463 8.769-1.357 15.764-4.489 18.582-9.603 2.818-5.117 3.236-6.578 3.236-6.578s8.353 11.797 15.556 13.155c7.203 1.357 28.605-5.952 28.605-5.952s13.051-3.549 15.346-8.038c2.297-4.489 8.353-19.209 8.353-19.209zM44.456 169.038l-.361-.166-2.013-1.736z" id="jkpath12" fill="#cb3349"/><g id="jkg97"><path class="jkst1" d="M195.832 70.085a48.125 48.125 0 0 0-.21-2.009 26.472 26.472 0 0 0-.215-1.424c-1.793-1.509-3.831-2.851-5.952-4.071-2.299-1.343-4.704-2.546-7.159-3.663-2.438-1.15-4.942-2.191-7.461-3.207a134.313 134.313 0 0 0-3.798-1.477c-1.269-.495-2.55-.956-3.835-1.424 2.697.447 5.366 1.059 8.015 1.741 1.723.446 3.437.945 5.14 1.477-12.112-31.655-41.07-52.27-72.687-52.27-31.622 0-60.577 20.615-72.686 52.27a109.044 109.044 0 0 1 5.137-1.477c2.653-.682 5.323-1.294 8.018-1.741-1.289.468-2.567.929-3.84 1.424-1.267.472-2.536.967-3.798 1.477-2.519 1.016-5.016 2.057-7.46 3.207-2.45 1.117-4.857 2.32-7.156 3.663-2.121 1.219-4.157 2.562-5.957 4.071-.075.457-.151.951-.21 1.424a51.768 51.768 0 0 0-.21 2.009 51.354 51.354 0 0 0-.177 4.061 59.216 59.216 0 0 0 .5 8.11c.37 2.692.864 5.366 1.595 7.951.36 1.295.768 2.572 1.24 3.808.237.617.495 1.225.764 1.816.134.294.274.585.413.864l.172.328c.199.101.408.204.607.3l1.204.575c.671.305 1.6.746 2.368 1.09.043-.037.086-.075.123-.114l-2.235-8.513c.474-.13 4.718-1.225 12.032-2.617a38.816 38.816 0 0 1-1.772-.381c-1.665-.414-3.309-.919-4.899-1.564a22.415 22.415 0 0 1-2.309-1.115c-.742-.426-1.472-.908-2.037-1.548 8.036 2.622 24.64 1.434 39.399-.091 13.499-1.391 27.029-2.293 40.63-2.32 13.602.027 27.137.929 40.63 2.32 14.766 1.525 31.37 2.713 39.405.091-.564.64-1.293 1.123-2.035 1.548a22.5 22.5 0 0 1-2.308 1.115c-1.592.645-3.234 1.15-4.899 1.564-.247.059-.496.113-.743.166 8.02 1.488 12.689 2.697 13.188 2.831l-2.138 8.11c.43-.194.864-.381 1.29-.574l1.202-.575c.2-.097.403-.199.607-.3l.166-.328c.146-.279.286-.57.419-.864.27-.591.528-1.199.764-1.816a42.235 42.235 0 0 0 1.241-3.808c.731-2.585 1.225-5.259 1.595-7.951.345-2.685.526-5.398.501-8.11a50.874 50.874 0 0 0-.179-4.059z" id="jkpath14" fill="#f4edae"/><path class="jkst2" d="M116.787 182.661c-1.064.16-2.128.295-3.186.375-.682.033-1.404.102-2.059.102l-.242.005c.822-1.837 1.446-3.26 1.919-4.339.963 1.08 2.188 2.417 3.568 3.857z" id="jkpath16" fill="#e6ccad"/><path class="jkst2" d="M119.101 185.018c3.304 3.272 7.398 5.146 11.904 5.479-7.569 3.074-14.702 4.26-20.197 4.63-5.478.367-11.032-.279-16.474-1.771.456-.082.79-.14 1.193-.189.447-.054 10.206-1.327 14.605-7.868l.413.009 1.08-.009c.731 0 1.395-.06 2.094-.087a43.69 43.69 0 0 0 4.878-.703c.167.171.333.338.504.509z" id="jkpath18" fill="#e6ccad"/><path class="jkst3" d="M128.464 87.071a98.82 98.82 0 0 1-1.048 1.343c-1.933 2.444-4.614 5.57-7.794 8.627a369.585 369.585 0 0 0-11.404-.177c-6.46 0-12.655.171-18.537.457 8.311-3.449 18.296-6.818 29.109-8.842a113.323 113.323 0 0 1 9.674-1.408z" id="jkpath20" fill="#656c67"/><path class="jkst3" d="M79.821 90.792c-2.966 2.084-6.317 4.744-9.566 7.971a360.155 360.155 0 0 0-21.567 2.81c9.207-4.232 19.713-8.127 31.133-10.781z" id="jkpath22" fill="#656c67"/><path class="jkst3" d="M181.48 107.969l-3.384 23.679-16.212 11.355-42.283-4.807-6.365-20.961a1.383 1.383 0 0 0-1.108-.971c-1.567-.253-2.953-.382-4.108-.382-1.16 0-2.541.129-4.115.382-.522.086-.95.461-1.106.971l-6.209 20.45-42.047 9.357-16.662-11.672-3.283-26.572c.715-.404 1.441-.806 2.176-1.209 1.031-.222 2.191-.457 3.475-.704l3.094 25.073c.048.392.264.741.586.967l11.462 8.032a1.425 1.425 0 0 0 1.101.213l34.57-7.692c.119-.027.237-.069.344-.124a1.39 1.39 0 0 0 .682-.827l6.225-20.498c1.67-.43 5.947-1.429 9.706-1.429 3.749 0 8.03.999 9.701 1.429l6.225 20.498c.161.532.624.912 1.176.977l34.57 3.927c.335.037.677-.05.952-.242l11.469-8.025c.31-.22.52-.566.573-.946l3.062-21.421c2.301.444 4.224.846 5.733 1.172z" id="jkpath24" fill="#656c67"/><path class="jkst3" d="M185.751 93.119l-2.976 11.29c-6.086-1.342-19.456-3.975-37.654-5.747 5.946-2.535 12-5.715 17.531-9.69 10.829 1.53 18.78 3.169 23.099 4.147z" id="jkpath26" fill="#656c67"/><g id="jkg32"><path class="jkst4" d="M63.841 128.441c2.357-1.274 5.021-1.085 9.19-1.079.447.011.908.005 1.39-.005.41-.005.822-.011 1.258-.022 4.296-.042 7.869.366 7.806-6.381-.065-6.746-3.062-12.198-7.354-12.155-4.297.037-8.454 5.564-8.197 12.306.07 1.756.328 3.023.742 3.937-3.745.938-4.777 3.254-4.835 3.399zm51.657-27.749a46.634 46.634 0 0 1-5.249 3.712l-6.097 3.68a52.065 52.065 0 0 0-7.331 1.467 1.216 1.216 0 0 0-.317.14 1.406 1.406 0 0 0-.629.794l-6.209 20.46-33.185 7.38-10.452-7.321-3.041-24.634c5.936-1.09 13.874-2.352 23.41-3.42a56.802 56.802 0 0 0-2.955 3.855l-5.677 8.149 8.266-5.511c.123-.086 5.387-3.549 13.998-7.761a377.407 377.407 0 0 1 35.468-.99z" id="jkpath28" fill="#e5caa3"/><path class="jkst4" d="M151.835 125.675c-.042-.16-.945-2.873-4.942-2.397.461-1.003.666-2.356.521-4.21-.528-6.731-4.443-12.08-8.735-11.931-4.292.152-7.042 5.731-6.805 12.478.236 6.741 3.84 6.694 8.132 6.543 5.77-.107 8.939-1.88 11.829-.483zm21.18-19.385l-2.992 20.944-10.539 7.379-33.141-3.766-6.183-20.363a1.41 1.41 0 0 0-.945-.934c-.205-.06-4.308-1.23-8.659-1.607l.795-.053c.687-.049 12.118-1.451 25.767-6.157 15.115 1.161 27.458 3.02 35.897 4.557z" id="jkpath30" fill="#e5caa3"/></g><g id="jkg38"><path class="jkst5" d="M63.841 128.441c2.357-1.274 5.021-1.085 9.19-1.079.447.011.908.005 1.39-.005.41-.005.822-.011 1.258-.022 4.296-.042 7.869.366 7.806-6.381-.065-6.746-3.062-12.198-7.354-12.155-4.297.037-8.454 5.564-8.197 12.306.07 1.756.328 3.023.742 3.937-3.745.938-4.777 3.254-4.835 3.399zm51.657-27.749a46.634 46.634 0 0 1-5.249 3.712l-6.097 3.68a52.065 52.065 0 0 0-7.331 1.467 1.216 1.216 0 0 0-.317.14 1.406 1.406 0 0 0-.629.794l-6.209 20.46-33.185 7.38-10.452-7.321-3.041-24.634c5.936-1.09 13.874-2.352 23.41-3.42a56.802 56.802 0 0 0-2.955 3.855l-5.677 8.149 8.266-5.511c.123-.086 5.387-3.549 13.998-7.761a377.407 377.407 0 0 1 35.468-.99z" id="jkpath34" fill="#c7b39a"/><path class="jkst5" d="M151.835 125.675c-.042-.16-.945-2.873-4.942-2.397.461-1.003.666-2.356.521-4.21-.528-6.731-4.443-12.08-8.735-11.931-4.292.152-7.042 5.731-6.805 12.478.236 6.741 3.84 6.694 8.132 6.543 5.77-.107 8.939-1.88 11.829-.483zm21.18-19.385l-2.992 20.944-10.539 7.379-33.141-3.766-6.183-20.363a1.41 1.41 0 0 0-.945-.934c-.205-.06-4.308-1.23-8.659-1.607l.795-.053c.687-.049 12.118-1.451 25.767-6.157 15.115 1.161 27.458 3.02 35.897 4.557z" id="jkpath36" fill="#c7b39a"/></g><path class="jkst2" d="M187.481 115.502c.508.419.911 1.504.456 6.558-.559 6.188-3.16 17.049-4.771 18.8-1.778.344-5.505-.064-7.778-.595.393-1.559.505-2.306.822-3.9l3.975-2.781c.317-.22.526-.566.58-.941l2.778-19.466c1.686.912 3.421 1.899 3.938 2.325z" id="jkpath40" fill="#e6ccad"/><path class="jkst2" d="M40.937 140.908c.199.704.408 1.407.624 2.1-2.139.628-6.495 1.23-8.465.886-1.633-1.645-4.679-12.966-5.345-18.978-.543-4.871-.162-5.924.333-6.334.575-.483 2.728-1.708 4.593-2.707l2.519 20.449c.048.393.257.741.586.967z" id="jkpath42" fill="#e6ccad"/><path class="jkst2" d="M121.347 141.194l-.151 1.305s-4.581 4.248-11.956 5.199c-7.375.95-13.171-3.582-13.171-3.582.242.788.586 2.567 2.256 4.086a53.184 53.184 0 0 0-6.313-.393c-.804 0-1.616.023-2.401.061-4.539.237-10.924 7.1-15.414 14.014-2.203.697-9.089 2.883-17.06 5.237-7.44-10.309-11.098-20.842-11.469-21.932l.005-.006c-.15-.419-.301-.839-.441-1.268l1.913 1.338v.005l4.726 3.309 1.58 1.101c.236.167.515.253.794.253.102 0 .204-.011.305-.031l43.435-9.67a1.385 1.385 0 0 0 1.025-.95l6.194-20.39c1.069-.145 2.008-.22 2.814-.22.801 0 1.746.075 2.815.22l6.374 20.997c.162.532.624.919 1.171.977z" id="jkpath44" fill="#e6ccad"/><path class="jkst2" d="M170.926 140.066l1.402-.984c-.232.973-.484 1.94-.747 2.896-1.949 6.248-4.25 11.774-6.805 16.656-.565.039-1.161.061-1.8.061-1.972 0-3.986-.167-6.215-.371-3.868-.355-10.007-1.058-11.946-1.283-1.67-1.332-7.385-5.873-12.14-9.615-.187-.151-.348-.291-.505-.42-.837-.708-1.789-1.513-3.717-1.513-1.751 0-4.308.638-10.489 2.508 3.212-2.401 3.233-5.5 3.233-5.5l.151-1.305 40.748 4.629a1.41 1.41 0 0 0 .955-.241l4.094-2.868z" id="jkpath46" fill="#e6ccad"/><path class="jkst6" d="M140.937 54.337c.124 3.625.033 10.194-1.655 16.345a1.335 1.335 0 0 0 0 .704 259.298 259.298 0 0 0-6.446-.591c2.412-5.054 2.938-10.436 3.052-12.332 1.852-1.317 3.696-2.896 5.049-4.126z" id="jkpath48" fill="#ebd599"/><path class="jkst6" d="M79.456 58.462c.112 1.896.638 7.267 3.046 12.317-2.149.171-4.297.37-6.441.596a1.328 1.328 0 0 0 0-.694c-1.686-6.139-1.772-12.714-1.654-16.345 1.353 1.231 3.19 2.81 5.049 4.126z" id="jkpath50" fill="#ebd599"/><path class="jkst7" d="M151.835 125.675c-2.89-1.396-6.059.377-11.828.484-4.292.151-7.896.198-8.132-6.543-.237-6.747 2.513-12.326 6.805-12.478 4.292-.15 8.207 5.2 8.735 11.931.145 1.854-.06 3.207-.521 4.21 3.996-.477 4.899 2.235 4.941 2.396zm-13.488-9.878a2.203 2.203 0 0 0 2.154-2.235 2.186 2.186 0 0 0-2.235-2.153 2.194 2.194 0 0 0 .081 4.388z" id="jkpath52" fill="#2d3136"/><circle transform="rotate(-1.049 138.093 113.428)" class="jkst8" cx="138.307" cy="113.602" id="jkellipse54" r="2.194" fill="#edf6fa"/><path class="jkst7" d="M83.484 120.953c.063 6.747-3.509 6.339-7.806 6.381-.435.011-.848.016-1.258.022-.482.011-.944.016-1.39.005-4.168-.005-6.833-.194-9.19 1.079.058-.145 1.09-2.461 4.835-3.4-.414-.914-.673-2.181-.742-3.937-.257-6.741 3.9-12.269 8.197-12.306 4.292-.042 7.289 5.411 7.354 12.156zm-6.634-3.529a2.195 2.195 0 1 0-.122-4.388 2.195 2.195 0 0 0 .122 4.388z" id="jkpath56" fill="#2d3136"/><circle transform="rotate(-1.473 76.78 115.216)" class="jkst8" cx="76.79" cy="115.23" id="jkellipse58" r="2.195" fill="#edf6fa"/><g class="jkst9" id="jkg64" opacity=".8"><path class="jkst6" d="M50.691 75.155s.667-8.692 2.03-12.023c.702-1.717 4.996-2.81 8.276-3.591 3.278-.78 8.508-2.342 9.524 2.264 1.015 4.606 2.653 7.963 3.746 9.446l-1.404-18.97-22.562 5.464-1.484 16.786.703 1.327 1.171-.703" id="jkpath60" fill="#ebd599"/><path class="jkst6" d="M164.855 75.155s-.666-8.692-2.029-12.023c-.703-1.717-4.997-2.81-8.275-3.591-3.28-.78-8.51-2.342-9.526 2.264-1.013 4.606-2.654 7.963-3.748 9.446l1.407-18.97 22.562 5.464 1.483 16.786-.703 1.327-1.171-.703" id="jkpath62" fill="#ebd599"/></g><path class="jkst10" d="M132.965 18.378s-.598 45.49-11.224 45.49h-14.875-12.752c-10.626 0-11.484-45.47-11.484-45.47l-5.22 15.438.085 21.183 3.707 2.947 1.685 9.096 2.357 5.307 45.482.084 2.105-3.791 1.769-6.4.254-4.043 5.023-14.341z" id="jkpath66" opacity=".75" fill="#ebd599"/><path class="jkst10" d="M166.429 60.794s2.187 15.692 7.974 18.522c5.788 2.829 0 0 0 0l-8.103-2.444z" id="jkpath68" opacity=".75" fill="#ebd599"/><path class="jkst10" d="M48.908 60.794s-2.187 15.692-7.975 18.522c-5.788 2.829 0 0 0 0l8.104-2.444z" id="jkpath70" opacity=".75" fill="#ebd599"/><path class="jkst7" d="M167.987 76.8c2.755.902 5.526 1.858 8.036 3.325-1.343-.532-2.729-.913-4.126-1.257a70.385 70.385 0 0 0-4.201-.924c-2.82-.531-5.65-.982-8.498-1.327-2.841-.37-5.687-.682-8.546-.924-2.858-.241-5.709-.483-8.573-.65-11.446-.704-22.924-.88-34.41-.892-11.483.006-22.962.221-34.409.897-2.862.166-5.715.409-8.572.651-2.857.241-5.71.548-8.546.923-2.847.345-5.678.796-8.498 1.327-1.407.264-2.81.57-4.206.919-1.391.344-2.783.725-4.126 1.257 2.509-1.466 5.28-2.427 8.041-3.331.232-.075.467-.139.703-.214-.015-.059-.032-.113-.043-.177-.048-.317-1.069-7.859.709-18.645.086-.516.456-.935.962-1.075l2.917-.831c.634-22.625 9.952-33.266 10.243-33.594-8.326 13.397-8.25 29.286-8.106 32.986l18.128-5.152c.016-.005.026-.005.042-.01.076-.016.151-.027.226-.032.021 0 .049-.006.075-.006a1.19 1.19 0 0 1 .297.027c.015 0 .031.011.053.016.075.016.145.042.224.075.033.016.054.033.086.049.058.033.119.07.177.112.016.011.034.016.049.033l.032.032c.016.016.037.027.054.044.012.016.494.493 1.262 1.209-.182-5.973.102-23.108 8.262-37.31-.172.498-6.646 19.428-4.415 40.645.724.58 1.486 1.149 2.229 1.649.359.247.58.655.585 1.09.006.07.161 6.833 3.148 12.586.042.086.074.177.102.268 7.429-.505 14.878-.709 22.312-.714 7.436.005 14.88.22 22.307.731.027-.097.06-.193.109-.285 2.986-5.753 3.142-12.516 3.142-12.586.01-.436.231-.843.591-1.09.741-.5 1.493-1.069 2.224-1.649 2.234-21.217-4.24-40.147-4.411-40.645 8.153 14.201 8.444 31.336 8.262 37.31a62.536 62.536 0 0 0 1.261-1.209c.016-.016.039-.027.053-.044.012-.01.018-.021.033-.032.016-.016.033-.022.049-.033.06-.042.119-.079.177-.118.028-.01.054-.027.081-.043.081-.033.155-.059.236-.08.016 0 .033-.011.049-.011.096-.021.2-.032.296-.027.027 0 .049.006.07.006.075.005.156.016.231.032.012.006.028.006.042.01l18.129 5.152c.146-3.7.221-19.59-8.104-32.986.289.328 9.609 10.969 10.237 33.594l2.922.831c.499.14.875.559.962 1.075 1.777 10.786.752 18.328.708 18.645-.01.065-.026.124-.042.182.239.07.47.139.707.215zm-3.297-.968c.14-1.207.789-7.809-.591-16.801l-20.52-5.833c.184 3.475.265 11.012-1.707 18.199a1.619 1.619 0 0 1-.101.258c.203.021.408.037.606.064 5.769.661 11.511 1.584 17.189 2.83 1.712.398 3.426.823 5.124 1.283zm-25.409-5.151c1.688-6.15 1.779-12.72 1.655-16.345-1.353 1.23-3.197 2.809-5.049 4.125-.114 1.896-.64 7.278-3.052 12.332 2.149.173 4.298.366 6.446.591a1.33 1.33 0 0 1 0-.703zm-56.78.098c-2.408-5.05-2.934-10.422-3.046-12.317-1.858-1.316-3.696-2.895-5.049-4.125-.119 3.631-.032 10.206 1.654 16.345.065.237.058.473 0 .694 2.145-.227 4.292-.425 6.441-.597zm-8.933.864a1.65 1.65 0 0 1-.098-.247c-1.975-7.187-1.889-14.723-1.712-18.199L51.244 59.03c-1.38 8.982-.736 15.583-.597 16.797 1.703-.462 3.411-.887 5.131-1.284 2.835-.628 5.693-1.154 8.556-1.638 2.869-.478 5.747-.843 8.626-1.192.205-.027.404-.042.608-.07z" id="jkpath72" fill="#2d3136"/><g id="jkXMLID_1_"><g id="jkg78"><path class="jkst7" d="M129.293 18.973v17.025h-12.068v-4.974h-2.72v22.981h4.109v12.85H97.505v-12.85h4.092v-22.98h-2.711v4.974h-12.06V18.973zm-3.626 13.408v-9.789H90.443v9.789h4.816v-4.974h9.964v30.225h-4.1v5.606h13.865v-5.606h-4.1V27.407h9.964v4.974z" id="jkpath74" fill="#2d3136"/><path class="jkst0" id="jkpolygon76" fill="#cb3349" d="M101.123 57.632h4.1V27.407h-9.964v4.974h-4.816v-9.79h35.224v9.79h-4.816v-4.974h-9.964v30.225h4.1v5.606h-13.864z"/></g></g><path class="jkst3" d="M30.694 93.119c1.759-.399 4.136-.907 7.051-1.47a104.37 104.37 0 0 0-6.222 4.597z" id="jkpath83" fill="#656c67"/><path class="jkst5" d="M95.111 139.78s.492 3.165-3.938 4.519c-4.428 1.355-32.482 9.716-35.682 9.263-3.199-.451-11.319-5.874-11.319-5.874l-1.969-7.004 12.016 7.492z" id="jkpath85" fill="#c7b39a"/><path class="jkst5" d="M120.242 139.167s-.354 3.182 4.131 4.345c4.484 1.161 32.875 8.295 36.05 7.704 3.176-.591 11.053-6.361 11.053-6.361l1.663-7.084-11.045 6.588z" id="jkpath87" fill="#c7b39a"/><path class="jkst5" d="M28.412 133.956s3.887 7.775 10.166 5.083l4.485 1.645-.448 3.29-9.419 1.195-2.541-1.494z" id="jkpath89" fill="#c7b39a"/><path class="jkst5" d="M187.551 131.822s-6.353 8.115-12.632 5.424l-2.019 1.302.448 3.289 9.419 1.196 2.54-1.495z" id="jkpath91" fill="#c7b39a"/><path class="jkst5" d="M89.279 192.904s23.03 11.611 49.106-4.188l-8.374-.571s-18.272 7.232-32.738 3.235z" id="jkpath93" fill="#c7b39a"/><path class="jkst7" d="M112.626 171.509l1.594 1.899c.036.046 3.577 4.26 7.906 8.552 2.879 2.853 6.357 4.297 10.343 4.297 1.361 0 2.791-.175 4.235-.523 1.34-.326 2.796-.673 4.287-1.03 5.384-1.287 11.482-2.749 14.438-3.577.585-.166 1.238-.315 1.925-.472 3.935-.909 9.329-2.163 12.187-7.889 2.149-4.297 5.047-9.874 7.197-13.961-1.863.859-3.816 1.79-5.203 2.52-2.138 1.123-4.938 1.667-8.558 1.667-2.152 0-4.266-.181-6.605-.389-4.675-.43-12.586-1.361-12.667-1.372l-.606-.067-.478-.383c-.071-.052-7.003-5.575-12.606-9.981-.227-.186-.434-.358-.621-.513-.59-.503-.59-.503-.942-.503-1.797 0-7.02 1.62-18.462 5.167l-.703.223-.689-.26c-.078-.026-7.585-2.81-16.581-2.81-.736 0-1.47.019-2.185.056-.901.046-5.958 2.448-12.425 12.68l-.419.657-.741.238c-.107.037-11.238 3.63-23.042 7.005l-.766.218-.725-.337c-.077-.031-4.696-2.174-9.091-4.194 2.397 3.541 5.462 7.958 8.159 11.422 4.711 6.067 10.649 11.674 22.034 11.674 1.428 0 2.945-.088 4.503-.265 11.581-1.309 14.563-1.837 16.168-2.117.543-.092.973-.171 1.522-.238.088-.011 9.571-1.237 12.232-7.206 2.744-6.134 3.298-7.595 3.319-7.651l.968-2.583s.12-.669.317-.877c0 .005 0 .005.005.005l.019.016c.305.219.757.902.757.902zM40.499 55.71c-2.516 1.014-5.016 2.06-7.46 3.209-2.449 1.119-4.856 2.32-7.155 3.66-2.121 1.222-4.157 2.563-5.954 4.076-.077.455-.149.952-.211 1.423a51.357 51.357 0 0 0-.388 6.068c-.026 2.713.16 5.426.502 8.112.372 2.692.864 5.369 1.594 7.952a41.963 41.963 0 0 0 1.243 3.804c.233.623.492 1.228.762 1.818.134.294.274.585.413.864l.172.326c.201.104.409.207.605.3l1.206.574c.673.311 1.6.751 2.366 1.093.046-.037.088-.078.124-.114l-2.231-8.511c.471-.129 4.717-1.227 12.032-2.619a33.744 33.744 0 0 1-1.775-.379 36.704 36.704 0 0 1-4.898-1.563 22.857 22.857 0 0 1-2.309-1.119c-.741-.425-1.471-.905-2.035-1.547 8.035 2.624 24.637 1.433 39.398-.088 13.501-1.393 27.028-2.293 40.628-2.325 13.6.031 27.138.931 40.63 2.325 14.77 1.522 31.374 2.713 39.406.088-.564.642-1.293 1.122-2.034 1.547-.739.42-1.522.782-2.309 1.119a36.965 36.965 0 0 1-4.903 1.563c-.244.056-.492.114-.741.166 8.02 1.486 12.689 2.697 13.186 2.832l-2.138 8.107c.43-.192.864-.377 1.288-.574l1.207-.574c.196-.094.404-.196.606-.3l.166-.326c.144-.279.284-.57.419-.864.27-.591.528-1.196.767-1.818.471-1.231.879-2.51 1.236-3.804.731-2.583 1.228-5.26 1.595-7.952.346-2.686.528-5.4.502-8.112a52.755 52.755 0 0 0-.176-4.059 51.573 51.573 0 0 0-.213-2.009 29.83 29.83 0 0 0-.213-1.423c-1.797-1.513-3.831-2.853-5.954-4.076-2.299-1.34-4.704-2.541-7.159-3.66-2.438-1.149-4.943-2.195-7.46-3.209a140.105 140.105 0 0 0-3.801-1.476c-1.267-.491-2.552-.956-3.835-1.423 2.696.445 5.369 1.06 8.013 1.739 1.724.446 3.444.948 5.141 1.481-12.11-31.658-41.07-52.272-72.685-52.272-31.622 0-60.576 20.614-72.684 52.272a107.832 107.832 0 0 1 5.135-1.481c2.651-.678 5.322-1.294 8.02-1.739-1.29.466-2.568.931-3.842 1.423-1.268.47-2.535.967-3.799 1.475zm159.43 18.316a53.972 53.972 0 0 1-.258 8.733 55.462 55.462 0 0 1-1.619 8.605c-.4 1.414-.86 2.811-1.404 4.198a38.295 38.295 0 0 1-.89 2.071c-.161.341-.331.678-.523 1.025l-.284.512a8.975 8.975 0 0 1-.348.574l-.294.457-.461.237c-.492.254-.895.445-1.342.653l-1.298.585a88.22 88.22 0 0 1-2.62 1.065c-.611.239-1.15.457-1.662.674l-1.444 5.487c-.036-.009-.471-.12-1.283-.315l-.078.574c1.594.833 4.726 2.522 5.793 3.403 2.148 1.775 2.299 4.587 1.823 9.841-.244 2.697-1.139 7.946-2.381 12.767-2.144 8.298-3.283 9.273-4.753 9.649-.746.192-1.894.383-3.008.383-2.266 0-5.353.063-7.429-.439-.533 1.888-2.055 6.812-5.068 12.962.151-.073.3-.135.435-.207 3.717-1.952 10.861-5.064 11.162-5.199l5.643-2.452-2.89 5.435c-.067.118-6.264 11.773-10.059 19.383-3.769 7.538-10.835 9.179-15.065 10.151-.637.151-1.241.291-1.733.425-3.035.854-9.18 2.319-14.599 3.623-.064.016-.13.033-.197.042a64.057 64.057 0 0 1-10.955 5.411c-14.568 5.518-29.923 5.208-43.844.092a647.05 647.05 0 0 1-9.193 1.097 45.12 45.12 0 0 1-4.985.291c-13.264 0-20.294-6.736-25.425-13.331-5.493-7.062-12.212-17.546-12.497-17.985L31 158.426l6.585 2.961c3.152 1.419 12.524 5.757 15.205 7 .217-.061.43-.124.642-.186-4.457-6.357-8.112-13.605-10.695-21.634-2.195.662-5.576 1.175-8.206 1.175-.961 0-1.822-.072-2.484-.228-1.471-.336-3.148-1.754-5.431-9.795-1.325-4.668-2.314-9.764-2.603-12.387-.57-5.121-.466-7.864 1.662-9.636 1.283-1.071 5.611-3.344 6.507-3.809l-.192-1.58c-13.75 8.08-21.991 15.22-22.157 15.366L0 134.302l7.005-11.047c5.544-8.755 11.948-15.832 17.84-21.284-.244-.098-.471-.196-.71-.294l-1.299-.585a34.907 34.907 0 0 1-1.34-.653l-.461-.237-.295-.457c-.166-.249-.238-.388-.347-.574l-.29-.512c-.181-.347-.358-.684-.518-1.025a30.878 30.878 0 0 1-.89-2.071 44.74 44.74 0 0 1-1.404-4.198 54.745 54.745 0 0 1-1.62-8.605 54.664 54.664 0 0 1-.259-8.733c.078-1.455.218-2.909.419-4.354.104-.725.213-1.45.358-2.17.15-.734.296-1.418.518-2.221l.155-.564.404-.317c2.294-1.802 4.768-3.163 7.284-4.369a78.87 78.87 0 0 1 6.311-2.616c5.943-16.493 16.162-31.118 29.591-41.311C74.337 5.57 90.664 0 107.671 0s33.334 5.57 47.218 16.106c13.43 10.193 23.649 24.819 29.588 41.307a78.282 78.282 0 0 1 6.316 2.62c2.515 1.206 4.99 2.567 7.283 4.369l.404.317.156.564c.227.803.372 1.487.517 2.221.146.72.26 1.445.357 2.17.203 1.443.348 2.897.419 4.352zm-11.995 48.031c.456-5.052.058-6.139-.455-6.554-.513-.43-2.247-1.412-3.935-2.329l-2.779 19.464a1.39 1.39 0 0 1-.58.942l-3.977 2.781c-.315 1.593-.429 2.345-.817 3.903 2.273.528 5.999.938 7.775.595 1.612-1.748 4.214-12.61 4.768-18.802zm-5.161-17.648l2.977-11.29c-4.318-.978-12.27-2.615-23.1-4.148-5.53 3.976-11.582 7.155-17.53 9.691 18.199 1.771 31.57 4.406 37.653 5.747zm-4.68 27.237l3.385-23.676a240.127 240.127 0 0 0-5.731-1.169l-3.059 21.422a1.415 1.415 0 0 1-.575.943l-11.472 8.023c-.27.192-.616.28-.947.243l-34.572-3.929a1.391 1.391 0 0 1-1.176-.973l-6.227-20.5c-1.668-.431-5.949-1.43-9.696-1.43-3.764 0-8.041.999-9.708 1.43l-6.228 20.5a1.388 1.388 0 0 1-1.025.947l-34.572 7.692a1.483 1.483 0 0 1-.306.033 1.36 1.36 0 0 1-.792-.25l-11.467-8.029a1.396 1.396 0 0 1-.585-.968l-3.091-25.072c-1.284.249-2.443.487-3.479.703-.734.405-1.46.809-2.174 1.213l3.281 26.568 16.666 11.675 42.047-9.354 6.207-20.449a1.389 1.389 0 0 1 1.108-.975c1.574-.253 2.95-.382 4.116-.382 1.153 0 2.536.129 4.105.382.528.083.957.461 1.108.975l6.366 20.956 42.282 4.808zm-8.07-4.411l2.992-20.948c-8.439-1.536-20.78-3.394-35.897-4.554-13.647 4.707-25.077 6.108-25.766 6.155l-.797.057c4.353.374 8.454 1.544 8.66 1.605.452.135.804.481.944.933l6.186 20.366 33.138 3.764zm2.303 11.845l-1.404.983-3.779 2.651-4.095 2.868c-.279.192-.621.28-.954.243l-40.746-4.633-2.966-.337a1.39 1.39 0 0 1-1.171-.977l-6.377-20.998c-1.066-.145-2.014-.219-2.81-.219-.809 0-1.751.073-2.817.219l-6.192 20.392a1.383 1.383 0 0 1-1.025.946l-43.435 9.672c-.103.02-.206.03-.305.03-.279 0-.559-.083-.798-.253l-1.578-1.098-4.726-3.307v-.011l-1.91-1.335c.135.43.289.85.441 1.268l-.006.006c.368 1.092 4.028 11.622 11.467 21.929a873.96 873.96 0 0 0 17.057-5.234c4.488-6.917 10.877-13.777 15.418-14.014a51.12 51.12 0 0 1 2.402-.061c2.221 0 4.344.16 6.31.393-1.671-1.517-2.013-3.298-2.256-4.085 0 0 5.793 4.53 13.17 3.584 7.378-.953 11.959-5.204 11.959-5.204s-.021 3.102-3.236 5.503c6.182-1.869 8.739-2.511 10.489-2.511 1.931 0 2.883.808 3.717 1.519.161.129.322.268.507.419a3519.302 3519.302 0 0 1 12.141 9.614c1.936.227 8.075.926 11.943 1.283 2.23.201 4.245.372 6.217.372.637 0 1.233-.026 1.797-.063 2.558-4.88 4.857-10.411 6.808-16.653.261-.96.516-1.928.743-2.901zm-15.034-51.593c-.01-.006-.02-.012-.031-.012a551.624 551.624 0 0 0-9.826-.651 905.6 905.6 0 0 0-13.667-.668 72.95 72.95 0 0 1-1.574 2.225c-2.479 3.355-7.398 9.51-13.704 14.729 8.926-1.6 24.409-5.56 37.803-14.905.336-.238.668-.486.999-.718zm-29.876.926c.377-.471.729-.926 1.044-1.34-3.281.331-6.512.808-9.67 1.408-10.814 2.024-20.801 5.389-29.11 8.837a383.259 383.259 0 0 1 18.54-.455c3.908 0 7.708.067 11.404.176 3.179-3.056 5.861-6.182 7.792-8.626zm3.587 102.085c-4.503-.332-8.598-2.205-11.903-5.477a271.86 271.86 0 0 0-.502-.512 44.25 44.25 0 0 1-4.881.704c-.698.026-1.361.087-2.091.087l-1.083.011-.413-.011c-4.396 6.539-14.159 7.813-14.605 7.87-.403.046-.734.103-1.191.186 5.442 1.491 10.996 2.138 16.474 1.77 5.492-.367 12.627-1.558 20.195-4.628zm-17.4-7.461a45.604 45.604 0 0 0 3.184-.378 138.958 138.958 0 0 1-3.568-3.857 398.441 398.441 0 0 1-1.92 4.339h.243c.658.001 1.378-.071 2.061-.104zm-3.354-78.632c1.827-1.103 3.582-2.366 5.249-3.712a422.33 422.33 0 0 0-7.278-.072c-10.137 0-19.606.415-28.189 1.061-8.61 4.209-13.875 7.672-13.998 7.76l-8.268 5.514 5.679-8.149a52.452 52.452 0 0 1 2.956-3.857c-9.536 1.066-17.477 2.329-23.41 3.422l3.038 24.632 10.453 7.321 33.184-7.378 6.212-20.464c.104-.337.331-.621.627-.793.098-.063.202-.109.315-.14.192-.052 3.51-.999 7.336-1.465zm3.816-18.788c-2.31-.036-4.623-.057-6.933-.062h-.005c-3.39.005-6.787.041-10.189.109l-6.269 2.971c-.005.005-.041.021-.088.048-.942.46-9.174 4.613-16.919 12.021 6.943-3.65 17.146-8.418 29.153-12.115a144.186 144.186 0 0 1 11.25-2.972zM70.251 98.761c3.251-3.225 6.605-5.886 9.567-7.967-11.415 2.651-21.923 6.543-31.128 10.778a360.846 360.846 0 0 1 21.561-2.811zm2.159-9.949a150.122 150.122 0 0 1 11.813-2.796c-5.798.212-11.6.481-17.393.808-3.366.186-6.715.414-10.065.667-1.678.129-3.345.263-5.007.445-.476.046-.942.098-1.418.16-4.369 2.614-21.127 13.134-32.631 26.889 11.179-7.769 30.654-19.443 54.701-26.173zm-30.85 54.197a68.861 68.861 0 0 1-.621-2.102l-5.162-3.612a1.391 1.391 0 0 1-.586-.969l-2.516-20.449c-1.864.999-4.017 2.225-4.592 2.707-.497.409-.875 1.46-.336 6.332.668 6.01 3.712 17.333 5.348 18.979 1.968.347 6.327-.258 8.465-.886zm-3.815-51.36a229.005 229.005 0 0 0-7.051 1.47l.829 3.127a103.93 103.93 0 0 1 6.222-4.597z" id="jkpath95" fill="#2d3136"/></g></g></symbol><symbol viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="tune" xmlns="http://www.w3.org/2000/svg"><path d="M6.85 2.852h-2v6h2v-6m12 0h-2v10h2v-10m-16 10h2v8h2v-8h2v-2h-6v2m12-6h-2v-4h-2v4h-2v2h6v-2m-4 14h2v-10h-2v10m4-6v2h2v4h2v-4h2v-2h-6z" fill="#fbc02d" fill-rule="nonzero"/></symbol><symbol viewBox="0 0 50 50" id="twig" xmlns="http://www.w3.org/2000/svg"><path d="M9.727 47.556c-.125-.223-.297-2.168-.183-2.087.034.025.171.267.304.537.132.27.282.487.332.482.123-.011.075-1.196-.1-2.454-.331-2.398-1.176-4.435-2.358-5.69-.2-.212-.344-.4-.319-.419.093-.067 1.327.843 1.842 1.359.293.293.735.825.981 1.181.328.474.465.618.51.534.078-.147-.21-9.903-.376-12.701-.074-1.255.063-1.023.61 1.035 1.064 4.006 1.858 7.922 2.342 11.55.086.637.173 1.172.195 1.19.022.016.092.001.157-.034.888-.483 1.524-.667 2.55-.736.727-.048.945.062.35.178-1.15.222-1.99 1.013-2.344 2.201-.315 1.061-.327 2.707-.024 3.434.152.366.037.426-1.067.56-.716.088-.977.096-1.202.037-.356-.092-1.118-.098-1.195-.008-.031.036-.243.066-.47.066-.38 0-.423-.017-.535-.215zm1.974-3.233c.152-.205.072-.41-.204-.522-.225-.09-.263-.088-.437.025-.21.137-.252.43-.08.554.18.13.607.096.72-.057zm1.248.086a.763.763 0 0 0 .214-.203c.241-.33-.352-.622-.745-.366-.406.265.08.785.531.569zm2.288 3.094c-.033-.039.117-.387.334-.775.216-.387.411-.665.433-.618.07.152-.201 1.28-.33 1.372-.15.108-.354.117-.437.02zM8.2 47.092c-.29-.343-.221-.434.14-.182.176.123.321.263.321.31 0 .165-.279.087-.46-.128zm8.649-.145c0-.053.102-.18.227-.282.25-.204.312-.113.143.207-.095.18-.37.236-.37.075zm8.065-.827c-.243-.025-.48-.088-.527-.141-.11-.125-.114-3.043-.004-3.043.045 0 .132.149.193.331.127.38.228.42.31.124.094-.337.065-3.472-.039-4.297-.449-3.55-1.865-6.124-4.342-7.89-1.086-.774-2.653-1.436-4.047-1.711-.764-.15-.522-.224.598-.182 2.364.089 4.167.706 5.847 2.001a11.046 11.046 0 0 1 2.32 2.502c.453.682.64.854.64.584 0-.07.063-.882.139-1.805.679-8.26 2.396-15.1 4.984-19.86 1.86-3.422 5.108-6.817 7.885-8.244 1.397-.718 2.539-.988 4.02-.952.933.023 1.01.036 1.77.307a6.822 6.822 0 0 1 1.363.662c.612.407 1.309 1.004 1.235 1.058-.026.018-.343-.165-.705-.407-2.657-1.771-5.062-1.52-7.12.742-1.108 1.22-2.651 3.53-3.634 5.443-2.828 5.503-4.541 11.464-5.291 18.413-.163 1.509-.282 3.76-.195 3.703.032-.022.266-.52.518-1.108 1.597-3.723 3.578-6.428 5.79-7.908.672-.449 1.612-.904 1.715-.83.022.016-.172.22-.432.454-1.957 1.754-3.248 3.76-4.232 6.572-.938 2.68-1.366 5.588-1.368 9.3-.002 1.741.188 4.385.366 5.101.125.505.08.546-.585.546-.55 0-2.306.138-3.416.27-.414.05-.817.04-1.609-.036-.58-.056-1.129-.119-1.218-.14-.165-.037-.18-.014-.2.302-.01.186-.098.203-.728.139zm2.507-6.725c.294-.11.375-.22.375-.517 0-.63-1.309-.706-1.524-.088-.074.211.13.51.42.616.297.108.413.106.73-.011zm2.369-.052c.277-.222.318-.364.174-.611-.4-.691-1.755-.307-1.428.404.121.266.299.35.738.354.227 0 .387-.045.516-.147zm3.011 6.681c-.027-.05.088-.268.256-.484.879-1.135 1.22-1.544 1.284-1.544.04 0 .056.037.036.082l-.423.964c-.212.485-.445.924-.519.977-.169.122-.57.125-.634.005zm2.446-.596c0-.121.853-.683.896-.59.018.04-.056.209-.166.376-.168.259-.238.305-.464.305-.164 0-.266-.035-.266-.091zm-13.04-.124c-.177-.159-.493-.656-.462-.725.018-.038.248.1.512.309.264.207.457.405.428.438-.075.088-.371.074-.478-.022z" fill="#9bb92f" stroke-width=".078"/></symbol><symbol viewBox="0 0 500 500" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="typescript" xmlns="http://www.w3.org/2000/svg"><path d="M49 51h408v408H49V51zm246.669 314.879l19.463-1.702c.922 7.8 3.067 14.199 6.435 19.198 3.368 4.998 8.597 9.04 15.688 12.124 7.09 3.085 15.067 4.627 23.93 4.627 7.87 0 14.819-1.17 20.845-3.51 6.027-2.34 10.512-5.548 13.455-9.625 2.942-4.077 4.413-8.526 4.413-13.348 0-4.892-1.418-9.164-4.254-12.816-2.836-3.651-7.516-6.718-14.039-9.2-4.183-1.63-13.436-4.165-27.759-7.604s-24.355-6.683-30.099-9.732c-7.445-3.899-12.993-8.739-16.644-14.517-3.652-5.779-5.478-12.249-5.478-19.41 0-7.871 2.234-15.227 6.701-22.069 4.467-6.842 10.99-12.036 19.569-15.581 8.58-3.546 18.116-5.318 28.61-5.318 11.557 0 21.75 1.861 30.577 5.584 8.828 3.722 15.617 9.199 20.368 16.432 4.75 7.232 7.303 15.421 7.657 24.568l-19.782 1.489c-1.064-9.856-4.662-17.301-10.795-22.335-6.133-5.034-15.191-7.551-27.174-7.551-12.479 0-21.573 2.286-27.281 6.86-5.707 4.573-8.561 10.086-8.561 16.538 0 5.602 2.021 10.21 6.062 13.826 3.971 3.617 14.34 7.321 31.109 11.115 16.769 3.793 28.273 7.108 34.513 9.944 9.076 4.183 15.776 9.483 20.101 15.9 4.325 6.417 6.488 13.809 6.488 22.175 0 8.296-2.375 16.113-7.126 23.452-4.751 7.338-11.575 13.046-20.474 17.123-8.898 4.077-18.913 6.116-30.045 6.116-14.11 0-25.933-2.056-35.47-6.169-9.537-4.112-17.017-10.299-22.441-18.559-5.424-8.26-8.278-17.602-8.562-28.025zm-65.728 50.094V278.454h51.583v-18.399H157.938v18.399h51.37v137.519h20.633z" fill="#0288d1"/></symbol><symbol viewBox="0 0 500 500" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414" id="typescript-def" xmlns="http://www.w3.org/2000/svg"><path d="M457 459H49V51h408v408zM69 71v368h368V71H69z" fill="#0288d1"/><text x="342.219" y="344.544" font-family="ArialMT" font-size="12" fill="#0288d1" transform="translate(-6058.94 -5838) scale(18.1514)"><tspan style="-inkscape-font-specification:sans-serif" font-family="sans-serif" font-weight="400">TS</tspan></text></symbol><symbol viewBox="0 0 24 24" id="url" xmlns="http://www.w3.org/2000/svg"><path d="M16 6h-3v1.9h3a4.1 4.1 0 0 1 4.1 4.1 4.1 4.1 0 0 1-4.1 4.1h-3V18h3a6 6 0 0 0 6-6c0-3.32-2.69-6-6-6M3.9 12A4.1 4.1 0 0 1 8 7.9h3V6H8a6 6 0 0 0-6 6 6 6 0 0 0 6 6h3v-1.9H8c-2.26 0-4.1-1.84-4.1-4.1M8 13h8v-2H8v2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="verilog" xmlns="http://www.w3.org/2000/svg"><path d="M17.282 17.08H6.718V6.513h10.564m4.226 4.226V8.627h-2.113V6.514c0-1.173-.95-2.113-2.113-2.113H15.17V2.288h-2.113v2.113h-2.112V2.288H8.83v2.113H6.718c-1.173 0-2.113.94-2.113 2.113v2.113H2.492v2.113h2.113v2.113H2.492v2.113h2.113v2.113a2.113 2.113 0 0 0 2.113 2.113H8.83v2.113h2.113v-2.113h2.112v2.113h2.113v-2.113h2.113a2.113 2.113 0 0 0 2.113-2.113v-2.113h2.113v-2.113h-2.113V10.74m-6.339 2.113h-2.112V10.74h2.112m2.113-2.113H8.831v6.34h6.338z" fill="#ff7043" stroke-width="1.056"/></symbol><symbol viewBox="0 0 24 23.999999" id="vfl" xmlns="http://www.w3.org/2000/svg"><defs><style>.jra{fill:#f05223}.jrb{fill:url(#jra)}</style><radialGradient id="jra" cx="205.45" cy="208.29" r="225.35" gradientTransform="matrix(.04556 0 0 .0456 2.888 2.88)" gradientUnits="userSpaceOnUse"><stop stop-color="#ffd104" offset="0"/><stop stop-color="#faa60e" offset=".35"/><stop stop-color="#f05023" offset="1"/></radialGradient></defs><title>houdinibadge</title><g stroke-width=".046"><path class="jra" d="M19.97 3H4.03A1.03 1.031 0 0 0 3 4.031v4.135C4.548 6.977 6.563 6.21 8.948 6.21c5.107.003 8.35 3.574 8.348 8.081 0 3.13-1.46 5.485-3.746 6.71h6.42A1.03 1.031 0 0 0 21 19.968V4.031a1.03 1.031 0 0 0-1.03-1.03z" fill="#f4511e"/><path class="jrb" d="M3 17.722v2.247A1.03 1.031 0 0 0 4.03 21h1.837C4.474 20.21 3.49 19 3 17.722z" fill="url(#jra)"/><path class="jra" d="M8.948 8.231c-2.586-.09-4.598.86-5.948 2.264v3.163c.918-2.654 3.447-3.87 5.565-3.85 2.647.027 4.689 2.025 4.7 4.284.012 2.159-.892 3.748-3.33 4.14-1.33.213-3.411-.567-3.318-2.578.046-1.037.854-1.622 1.777-1.58-.905 1.213.293 2.102 1.139 1.921 1.048-.224 1.475-1.156 1.475-1.878 0-.762-.718-1.994-2.498-1.951-2.204.052-3.591 1.639-3.638 3.602-.056 2.468 2.253 4.091 4.622 4.121 3.48.046 5.543-2.24 5.539-5.586-.005-3.029-2.434-5.946-6.085-6.072z" fill="#f05223"/></g></symbol><symbol viewBox="0 0 24 24" id="virtual" xmlns="http://www.w3.org/2000/svg"><path d="M21 14H3V4h18m0-2H3c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h7l-2 3v1h8v-1l-2-3h7a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 281.25 281.25" id="visualstudio" xmlns="http://www.w3.org/2000/svg"><path d="M196.18 101.74l-52.778 42.444 52.778 40.889V101.74m-136.67 110l-30-18.889v-100L62.843 81.74l47.778 37 96.666-89.222 44.444 27.778v172.22l-55.555 22.222-85.111-81.555-51.555 41.555m3.333-48.889l20.667-19.111-20.667-19.778z" fill="#ab47bc" stroke-width="11.111"/></symbol><symbol viewBox="0 0 300 300" id="vscode" xmlns="http://www.w3.org/2000/svg"><defs><style>.icon-canvas-transparent{fill:#f6f6f6;opacity:0}.icon-white{fill:#fff}</style></defs><title>BrandVisualStudioCode</title><path d="M218.62 29.953l-105.41 96.92L54.301 82.47 29.955 96.64l58.068 53.359-58.068 53.359 24.346 14.212 58.909-44.402 105.41 96.878 51.424-24.976V54.93zm0 63.744v112.6l-74.719-56.302z" fill="#2196f3" stroke-width="17.15"/></symbol><symbol viewBox="0 0 24 24" id="vue" xmlns="http://www.w3.org/2000/svg"><path d="M1.821 4.15l10.21 17.618L22.24 4.235V4.15h-7.692L12.113 8.33 9.691 4.15H1.82z" fill="#41b883"/><path d="M5.937 4.15l6.152 10.616 6.18-10.617h-3.722l-2.434 4.179-2.422-4.179H5.937z" fill="#35495e"/></symbol><symbol viewBox="0 0 420 419" id="watchman" xmlns="http://www.w3.org/2000/svg"><g stroke="#fff" stroke-linecap="round" stroke-linejoin="bevel"><path d="M166.95 145.32a93.935 123.23 0 0 1 92.934 3.263" fill="none" stroke-width="18.467"/><path d="M162.92 137.96L44.63 256.25a174.07 173.93 0 0 0 5.705 16.486l123.68-123.68-11.096-11.096zM266.54 144.04l-11.096 11.096 117.16 117.16a174.07 173.93 0 0 0 5.691-16.5l-111.76-111.76zm170.65 170.65v22.193l17.1 17.1 11.096-11.098-28.195-28.195z" fill="#fff" stroke-width="1.963"/><path d="M167.52 273.36a93.935 123.23 0 0 1 92.934-3.263" fill="none" stroke-width="18.467"/><path d="M49.516 144.56a174.07 173.93 0 0 0-.809 2.213 174.07 173.93 0 0 0-4.757 14.344 174.07 173.93 0 0 0-.016.055l119.56 119.56 11.098-11.096-125.07-125.07zM454.87 64.703l-17.668 17.668v22.191l28.764-28.764-11.096-11.096zm-80.984 80.984l-117.86 117.86 11.098 11.096 112.18-112.18a174.07 173.93 0 0 0-5.416-16.777z" fill="#fff" stroke-width="1.963"/></g><image x="21.229" y="20.262" width="378" height="377.1" preserveAspectRatio="none" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAaQAAAGjCAYAAABjSWGNAAAgAElEQVR4AeydB3hUVdrH/+fOpNMF JAFUmivFXtZuIBRRQUUTil1RV3et6Lr6rSu6rg3B1dXVtXeBCCioCASIimLDhkFsgAIJSAkhPZmZ 8z3vjReGMJlMueeWmfc88MzMLaf8zp3855zznvcV4MQEkoxA7sVr01PrUrv4pdZNBNA1AHQRkJ0g ZAcJrYOA7Aipv8/Q6JgUKdBkG0iZISDSDVwSaAfAY3z+/bVSAD56L+mfwA5INAqgSkrUCKA2IOQO IcQOIUU5pNwhNZQLia2Q2OyH3OJBxqaiwk4VzfLlj0wg4QmIhG8hNzDJCEgxJH9DjgfevgGBAyCx vxSyh5DoAWA/CHQH0MEFUOoBbJbAeiHEBiED6wMSv0DgF02In1J2Vq+ZP78fXcOJCSQMARakhOnK 5GrICaO3tE3P8B0kERgkgP6Qop8E+gqgL4Bdo5gEphKAwHpIrIHAjxL4DpCrAo1yVfHsHhsSuN3c tAQmwIKUwJ2bGE2TYsjY33qLQOBwCHk4JA6HhgE08kmM9ilpRQUgvwPE1xD4MiC0r2R67dfFz/eq U1IaZ8oETCLAgmQSSM7GHAIj8td38sN7HAROkpDHATgMTWs15hSQvLn4JLBaQHwqEPgQQiwvmpHz XfLi4JY7kQALkhN7JYnqlDd2w4GQnuMk5AkCOBHAQQD4ubTgGZDAFg1ieQCBj6Dhg7SKmhW8LmUB eC6iRQL8xW8RDZ8wn4AUQ8eVHib9YjCELj4nAOhqfjmcY4wE6iGxAkIsCwDvSel5v7iwa1WMefFt TCBqAixIUSPjG6IhkDtmQw/N4xkKTQ4TEkNZgKKhZ/u1DZD4CAKLIMSik/p3WzF5sgjYXiuuQMIS YEFK2K61p2GTJ0tt2Xebj5EyMArAab+vAdlTGS7VVAI0xSeAd6QQb2uBtIW8V8pUvJwZz9XzM2AG AdpoKqpTh3uEdlYA8gwBdDEjX87D0QQaALlMCLzha5Rz2NTc0X3lmsrxCMk1XeWsig6/YFOWr0Ge JoBzICWNhNo6q4ZcGwsJkFOKT4TUZiOgzSqate8aC8vmohKIAAtSAnWm6qaMHPljWl2bzBGapk2A lKMBZKguk/N3JYHPpMCrHuGbuWj6fqWubAFX2hYCLEi2YHdToVIMKSg7SQAXABgDoJObas91tZWA H5DvCeCV2rrUwg/ndqm0tTZcuOMJsCA5vovsqeDwszd19af4LwbEZQAOtKcWXGoCEaiCwAwB7emi Gd0+TqB2cVNMJMCCZCJMt2dFFnIflJQNh8BEgKbkRIrb28T1dySBlULIpzyBwCsLCntud2QNuVK2 EGBBsgW7swqlvUIer+cSQE7UPWI7q3pcm8QlUAtgtibFU4sKu70PCJm4TeWWRUKABSkSSgl5DTkt 3XSqJuVfJDAiRFyfhGw1N8qxBH4AxJP+hrpnit/otcOxteSKKSXAgqQUr/My18216wMXCOB6AH9w Xg25RklOoEoI8bzw+R5ZNKvnj0nOIumaz4KUJF0+YsyWbL/Xdx0gr5BAxyRpNjfTvQQCkPIdDdqD iwqz33NvM7jm0RBgQYqGlguvbfKmrd2MJrPtNBc2gauc5ASElJ8EIO4/eWD2m+xLL7EfBhakBO3f wfmlR2oCtwI4G4CWoM3kZiURASmwGpAPVLTPeXnFk6IxiZqeNE1lQUqwrs4bW3Y0AoF/QIjT2Vdh gnUuN8cgsFYK3LNPoPzFwsKBDcZBfnU/ARYk9/eh3oIh+WXHaELeIZs8bCdIq7gZTCAsgXUQuK9T oPw5FqawnFxzkgXJNV0VuqJD8zcdDCHvlvpG1tDX8FEmkOAE1gohJp/Yv9vLvMbk7p5mQXJp/w0Z u7mPkP7JACbwGpFLO5GrbS4BiRII/H3xzOw3eZOtuWityo0FySrSJpWTO760s8cv7wQEeVVINSlb zoYJJBAB+bGQ4uaiwpxlCdSopGgKC5JLuvm4/PUZmcJ7NSD/DqCDS6rN1WQCdhKY45f+vxYX9vzJ zkpw2ZETYEGKnJVNV1L4h9JxAuI+9jNnUxdwsW4mQFZ4//E31N/NLomc340sSA7uo2H5Gw7zC+3f AjjFwdXkqjEBNxDYKgVuPbl/9rNs+ODc7mJBcmDf5J61toMnNfVfgLgCgNeBVeQqMQF3EpD4XGja NRyTyZndx4LkqH6RYkj+pouEkFMAdHZU1bgyTCBxCEgIPFvvabx52av7lydOs9zfEhYkh/Th0HM2 95Ye//8ADHVIlbgaTCDRCWwWENcWzcyemegNdUv7WJBs7qncXOnVum66XoBMuZFpc3W4eCaQdAQE xFyfz//n4tk9NiRd4x3WYBYkGzuEjBak0J6WwJE2VoOLZgJMANgJIW49qX+3J9jowb7HgQXJBva0 pyhLeO+QkJPYaMGGDuAimUDLBJYJgSuKZuR81/IlfEYVARYkVWRbyHfouLJcBPCUhOzbwiV8mAkw AXsJ1EOKe3Z07HYvh7mwtiNYkCzinXvx2nRvTdq9EriOw0JYBJ2LYQLxEfgCUlywuDB7VXzZ8N2R EmBBipRUHNcNGVt6hJB4CcCAOLLhW5kAE7CeQB1tqF0yI/thdtiqHj4LkkLG+fnSUy7KbpWQ/wBE isKiOGsmwASUEpBLPBouXji9+3qlxSR55ixIih6A3DEbemhe7WV2+6MIMGfLBKwnsA3AxMUzc96w vujkKJEFSUE/D87fOFoT4lkA+yjInrNkAkzAVgLyv/7MhknFz/eqs7UaCVg4C5KJnTpy5I9p9W3b TBGQf2HDBRPBclZMwHkEVnr8KFg4K2e186rm3hqxIJnUd0PGlO4vPHgdAkeZlCVnwwSYgLMJVEHI yxfP6D7d2dV0T+1YkEzoq6H5ZadLIV/gKToTYHIWTMBlBIQQj6bsrLpp/vx+9S6ruuOqy4IUR5fo VnRa2Z1S4jaeoosDJN/KBNxP4DMhcW5RYc6v7m+KfS1gQYqRvR6zKCXtVQiMjDELvo0JMIEEIiCB LR4p8hcVZr+XQM2ytCmapaUlSGF5+WUDvKnpn7EYJUiHcjOYgAkEBNAlIAKLho7deI0J2SVlFjxC irLbh+SXni0EXgTQJspb+XImwASShICAeC6lsuoqXleKrsNZkKLglVdQeiuAf/F6URTQ+FImkLwE lnkatXMWzun2W/IiiK7lLEgR8KL9RQ1tsyia60URXM6XMAEmwAQMAmsDHjFq6WvZJcYBfm2ZAAtS y2z0M7njSzt7/JgD4MRWLuXTTIAJMIFQBCqkkAVLZnRfGOokH9tNgI0adrPY611u/vq+Xr9YzmK0 Fxo+wASYQOQE2gsp3h4ytnRi5Lck55UsSC30+5D8smM8wvMhB9JrARAfZgJMIBoCXiHxVN7YjXcC kmemWiDHYEKAyRu76QzIALkDyQpxmg8xASbABGImQBZ4vt+6XVFcLHwxZ5KgN7IgNevYvPyNl0EI MmDwNDvFH5kAE2AC5hCQmJ9Zh3PnzcupMSfDxMiFp+yC+nFIQdlNEOIpFqMgKPyWCTAB8wkIjKxJ xyLy+GJ+5u7NkUdIet9JkVdQRvuLaJ8RJybABJiAVQS+9kvvqcWFXTdZVaCTy0l6QZo8WWrLVpU9 JoE/ObmjuG5MgAkkJgEB8ZNPw7Di6dnrErOFkbcqqQWJvHVvF2UU2fXCyJHxlUyACTAB0wn86pf+ vOLCnj+ZnrOLMkzaNaTcXOndLja9zGLkoqeVq8oEEpfAfh7hfW/4OaUHJW4TW29ZUo6Q8vNLUreh 42tCYEzriPgKJsAEmIBlBDZDiiGLC7NXWVaigwpKOkEiMSoXnQol5GgH9QNXhQkwASagE6C4SprU 8ooKu61MNiRJNWVH03Q0MmIxSrbHnNvLBNxDgOIqSREoGjq2tL97am1OTZNmhEQGDNtE6asCosAc dJwLE2ACTEAlAVkGIXMXz+jxg8pSnJR3UoyQfreme57FyEmPHteFCTCB8ARENqS2mJw8h78ucc4m gSBJUS7KHgVwfuJ0G7eECTCBJCHQwyM8i3LHbOiRDO1NeEEaMrbsHt70mgyPMreRCSQsgQM8Hu3d kfllXRK2hb83LKEFaUjBxluExN8SvRO5fUyACSQ4AYGBjQjMG5q/vX0itzRhBSlvbOmVAuLeRO48 bhsTYALJQ0AK8UcpamfT1pVEbXVCCtLg/I2jIfEYgKSxIkzUB5TbxQSYQDABMWS76Phsogb5SzhB Gjxu0x81IV7jEBLBDzG/ZwJMIIEInJc3tuyhBGrPrqYklCCReaQIBOYByNzVQn7DBJgAE0g0AhLX Dc0vTbj18YSZ0iILlEaBjyRk0tjsJ9p3jNvDBJhAVAQkpBy/uLD7jKjucvDFCTFCokW+Bsg3WIwc /KRx1ZgAEzCbgIAQz9IyhdkZ25VfAgiSFPoin8DxdkHkcpkAE2ACNhHI1AKBuYmycdb1gjS0oOz/ AJxn08PAxTIBJsAE7CbQ1ePV3kmEPUquFqS8/LJzJXCX3U8Dl88EmAATsJnAwVLUv+R2c3CPzRBj Ln5o/qaDIeRbABJ2k1jMcPhGJsAEkpHAH3oPrNTWlkxb6tbGu9LKLu/sDfsgRfsEQB+3gud6MwEm wAQUEJBC4NyiGTmzFeStPEvXTdlRKAmkaLTxlcVI+ePBBTABJuAyAkJKvKDPILms4lRd1wlSudj0 LwDDXMiaq8wEmAATsIJAG6kFZrnRyMFVgkQ+6iTkX63oUS6DCTABJuBaAhL9Aqhznc871wjS0HM2 99aEeIEdprr2K8IVZwJMwEICQmBMXsGmGy0sMu6iXGHUkHvx2nRPddpHEDg87hZzBkyACTCBpCEg G4UUQ4oKc5a5ocmuGCF5a9IeYjFyw+PEdWQCTMBZBESKFGKGbpnsrIqFrI3jBWno2NIxHII8ZN/x QSbABJhABARkDlI8z7lh06yjBWnImNL9IfF0BMT5EibABJgAE2iRgByVl192bYunHXLCsWtItN9o O8reBztNdcijwtVgAkzA5QTqhSaPK5re/UuntsOxI6RyUXo7i5FTHxuuFxNgAi4kkBaQ4tVRo0od G8DUkYI0JL/sGAlBXrw5MQEmwASYgEkEhMRBtZnifpOyMz0bx03ZDb9gU5a/PvAFgANNby1nyASY ABNgAlIKeeqSGd0XOg2F40ZI/nr/AyxGTntMuD5MgAkkEAEhpHh2RP76Tk5rk6MEacjYjcMBcZXT IHF9mAATYAIJRqC7X3gedVqbHDNld8LoLW3T0xu/BbCf0yBxfZgAE2ACiUhASoxZUpgzxyltc8wI KT29cQqLkVMeC64HE2ACyUBACPnYiRN+6eiUtjpCkIbll50C4AqnQOF6MAEmwASSg4DITvN7pzml rbZP2ZFNfE0GvuGAe055JLgeTIAJJB0BiZGLC3Petbvdto+QajPEP1iM7H4MuHwmwASSmoDA407Y MGurIOWNX3+IRMBV8TqS+qHlxjMBJpCoBA6oyRB32N042wRp8mSpwe99AhApdkPg8pkAE2ACTEDe OHjshkPt5GCbIC0r2XQlII+zs/FcNhNgAkyACewi4NWkeEIfLOw6ZO0bWwRpZH5ZFynkPdY2lUtj AkyACTCB8ATEse9/V3Zp+GvUnbVFkBqaxKiDumZxzkyACTABJhATAYl7cs9aa8vfZ8sFadi40qMA 2KbAMXUQ38QEmAATSBICAuiipabfZUdzLRYkKQIB+R8AFpdrB1oukwkwASbgTgIC8uph+RsOs7r2 lgrDkPxNFwHiWKsbyeUxASbABJhAVAQ8AWgPRXWHCRdb5qnhd48MPwDobkK9OQsmwASYABNQTEAI eVbRjO5vKi5mV/aWjZBq0sVNLEa7uPMbJsAEmIDjCUgpHjjyCmnZXlFLBGnEmC3ZEPJmx9PnCjIB JsAEmEAwgQM7lJddHXxA5XtLBMmX0vhPAG1UNoTzZgJMgAkwAQUEBG63KkSFckEafk7pQZC4SAEm zpIJMAEmwATUE9gn1Z/yV/XFWGB+7feARkdeKxrDZTABJsAEmID5BITENXnjN+9rfs575qh0hPT7 Jthz9iySPzEBJsAEmIDLCGSJQODvquusVJACAZC/OstMy1XD4vyZABNgAslKQEp5xfD8Tb1Utl+Z IA0pKD0ZwDCVlee8mQATYAJMwDICqX7NTwFVlSVlgiQgbQ/2pIwaZ8wEmAATSEYCUpw/ZOzmPqqa rkSQ8saVngSIIaoqzfkyASbABJiALQS8Aj5la0lKBAkBOdkWVFwoE2ACTIAJqCUgxflDz9ncW0Uh pgvS0PzSE3l0pKKrOE8mwASYgCMIeOFRY3FnuiBB4FZHIONKMAEmwASYgBICEvK84eM29jQ7c1MF KW/8+kMkMNLsSnJ+TIAJMAEm4CgCqb6AuMHsGpkqSPB7yL0E7zsyu5c4PybABJiAwwgI4PIR+es7 mVkt0wQpd1zZAQDGmlk5zosJMAEmwAQcS6CNT3j/bGbtTBMkLYAb2WedmV3DeTEBJsAEnE5AXntc /voMs2ppitPT3LPWdhCQl5hVKc6HCdhNwOsVaN9WQ1amQFaGQEaGhox0gdQUAaEBaali19x0Q6PU j1OdG30S/gDQ0CDh9wPVtQFUVUlUVgdQXRNATa20u2lcPhMwk0DnLOE5H8BTZmRqiiBpqekTAcnx jszoEc7DUgIkLL3396JvrxT03T8FPXK8yNnXgy77eJTUo7pG4tdSH9Zv9OGXDT78vK4Rq39uRFV1 QEl5nCkTUE1AAtcD8mlAxP1rK24DhNxc6fV0LfsZwH6qG875M4F4CXTqoOHQAWk4dGAqBv0hFQf0 NOU3WbzV0gXqu58asXJ1A75YWY/NW/xx58kZMAGrCAjg1KKZOQviLS/ub6O3a9mZksUo3n7g+xUR SEkROKR/Kv54eJr+v3t23I+8kpr27O4F/R9+StN0/MZNPnzxTQM++7oeK76pR31D3D8+ldSbM2UC REBKXAUgbkGKe4SUl1+6FAK53C1MwCkEaK3n2CPSMfiEdBx/VLpTqhVXPZZ9WocPP6vDxyvq9fWo uDLjm5mA+QQCwu/pVzRr3zXxZB2XIOXllw2AkCXxVIDvZQJmEKCR0PFHpWHw8Rk48ZjEEKGWuJAw LXq/Fp98WY/GRh45tcSJj1tLQEDcXzQz+2/xlBqfIBVsfBQQptqhx9MYvjf5CPQ5IAUjB2fgrFOz kq/xAN4qqsE7i2vww5rGpGw/N9pRBLZ2kuXdCwsHNsRaq5gFadSo0syaDGwE0CHWwvk+JhALAbKM yzsxA6OGZ6Jfr5RYski4e0iQ5i2qwdIPa1FXz6OmhOtglzRISHleUWH3V2OtbsyCNLRg46US4plY C+b7mEC0BPbt4sGoYZkYdybvMAjHbtbb1Zi7qAYby3zhLuNzTMB8AhIfLC7MoWjhMaWYBElKeeCM udULXp1TeQDtq+DEBFQSGHRQKkYPy8SQE03bEK6yuo7J+73ldZi7sBpfr4p5BsUxbeGKuIOApkE+ PLnTjQMOSv93LDWOWpBIjAB8T4XNW1iDp1/bCRalWNDzPa0ROO7IdH1a7pjD0lq7lM+HIfDNqgZ9 xFT8UW2Yq/gUE4iPAE2fP35fZyOTvwsh/mV8iPQ1FkG6F8AuSwoWpUhR83WREsg9PkMfER0yIDXS W/i6CAjQxtt5C6t1Cz3JExsREONLIiXQe78UPDlllxgZt3mFEFHt8I5KkKSU5E9lr4lpFiWDP7/G Q+CUY2lElIXDBrIQxcMxknsfeqoCbxfVRHIpX8MEwhJoQYzontOEEPPD3tzsZLSCdCqAkAWQKP3v 5Z1s4dMMMH9snQBNyZHFHE3RcbKOwKofGnTLPNrTxIkJxEIgjBhRdjOFEFGFJIpWkF4EcEFLFSdR eviZipZO83EmsAeBA3unYPTwTJw6OHOP4/zBWgLkmohMxskbBCcmECkB8gP59INdWru8vRBiZ2sX GecjFiQpJf3VqDZubOmVRaklMnzcINCxvaabbp9zuns3s1IYCWMdJjNDoLau6XNKCnaFojDa65ZX GimRVd53P/ImW7f0mV31pC0Y40a30Wc2WqnDJUKI51u5ZtfpaASJhl7Td90Z5g2LUhg4SXxKCGD8 WW1w6bi2jqZAISHWl/qwcZMf5OR0yzY/tu8IoKIygIqdgV1CFK4RXg/QsYMHnTtq6NTRg25dPdi/ hxf75XhBZuxOTrSPqfCtamzdHtV6tJObxHUzkUAUYkSlLhJCDI+0+GgE6U0AoyPNmEUpUlLJcR15 Vrj1Guc59aA/urRP59vVDbr7nTW/+pT7hyNh7pHtxYADU3BQ31TdiKNnjvO8kD8/sxKvzK6KSICT 4ynmVkYpRgawbCHEJuNDuNeIBElK2RHA9nAZhTrHohSKSnIdoz+8Z52a6Shfcx9/UY9PvqjDFysb 9BGQE3qka2cPjjg4DUcflgayNnRSuu/RHSj6gA0fnNQndtQlRjGiql4jhHg0kjpHKkgUnvzZSDJs fg2LUnMiyfP5vDFtcMlYZ0zP0R/UpR/V4qtvGxwfW4g8lx91SBpOOCYNp+Y6w+CDnLjSd/nnX3h9 KXm+wbtb2r6thosL2kayZrT7pt3vioUQg3d/bPldpIL0NtmUt5xN+DMsSuH5JNpZp3hYIKuxhe/V 6kHu3BqmgcSJggtS4D4nxHZ66fUqvPh6JU/jJdqXNkx7sjIFJo5vF6sYGTnvK4T4zfjQ0murgiSl bA9gR0sZRHqcRSlSUu69zuMBLhvXDgWj7bOeW7fBh/lLavQpJjJASKTUrq2GYSdl4KqL2tnerH8+ VI73PmYzcds7QnEFTBIjquWVQognW6tuJII0HkDM7sSDK8CiFEwjsd4POzkDt/zZPqMFGg3NXViD L1bWJxbYEK0ho4hDB6TqXi3sXG+a/U41XplTpVsehqgmH3I5ARPFiEgsFEKMaA1JJII0E0B+axlF ep5FKVJS7riOwoVfPLYtzjnNnlHRzHnVmLeoGmWbk9NEOWdfjy5M+WfYw5+e0keeqdB/DLjjieVa RkKA9tZdPiHuabrmRXUQQoT1nNCqIAUCcqYQ5gkS1ZBFqXk/ufOznaOiFwsrMXt+DaqqE2taLtYn gf6AnD0yyzYjkjcX1GD2/GqOwRRrBzrsvusuax/vmlGoFh0phPgi1AnjWKuCNOutquvHnJ71kHGD Wa8sSmaRtD4fWiuiX0/n2vCr/IXCSsycW+14Sznre6WpRK9XYMxpmbjiPHvWmZ54aSdef6tVhy52 4eFyIyCgSIyoZE0IEdbPPHnvDpu2+f5y7uqfGo/PO8nc4Gh/6JOCju09+OTLxJ/zDwvYZSfJ0uvZ aV0w4EBrvQ2QEP3ffeX63iF/cs7ORfSkBAJAyfeN+tpOba3EkYdYG0vqqEPT0KG9B6Wb/dhZyaPX iDrNQRepEqM/3bK14sWnMqesWnVnfILUa9Ckh0s3+Tuv/qkRLEoOenJsqArtKbpuIhldWpdmzK3C P6aU47Ov6sFCFDl38rNX8kMjXn2jGo0+icMHWSdM9GPzrFOzUF0r2S9e5F1m+5WqxOiav28lLyjp dahatGbV1F/DNTTsCGnI2M19hJR3UQbk14tFKRzKxD3X94AUfVOclc5Q58yvxgOPV2Dph3U8PRfH o0UjppXfNWDGvGpoAjjYQj96Rx+ahvbtPPh5nU93PhtHM/hWxQRUidH1d2zDqh9+30wtsGltydQl 4ZoSVpB6DbxxrADOMDIgUfplgw+nHMfTdwaTRH+lX7p33dwR/XqnWNLU5Svq8PgLO/HGuzU85WMi cRpdfvltAxYva9o71L+fNVOuB/VNAVkA/rYtgJ/WsZcHE7vUtKyuubQ9Ro8w3yMIiRH5iAxKaWtL pj4d9Hmvt2GNGobkl84SAmOa30V7H26/gdzbmZvY0MFcnvHmRhswrTTnfvQ5EiJnLoiTFdthA9NA 01H7dfcip5sHbTI1tMnSdMwNjVIfBZBn8A2lPtAG3a9K6rH2170CLMfbLabcT/uYRg/PwinHWec3 j4wdyOiBk3MIkBidaY0YUaP9XunvuqCwZ4t+UVsUpPx86dkuyrYCCLnbUZUokfnof54Na6runN5M 0JrQegMFzjvpj9b8saJ1ohdmVoH+qDsppaUKDD4hA8NPzsAhA2IbUWwvD+CDT+vwzmJn+oEbNSzT 0nXBjz6v04MB0pogJ3sJqBKjG+7YhpV7jox2N1SK/MWF2a/vPrDnuxYFKS9/w3EQ2kd7Xr7nJxal PXkkwieaXrnyAmtMht9bTt4VqvXwD05iRzvUzz29DS44t42p1fr863p9Ayn9UXZS0jToa4QTzja3 veHa+NyMptAW4a7hc+oIqBKj2+7djk/D/dgQeHLxjJwrW2pZi2tIvQbedJEQGNLSjXSc1pNUrCnR vDMthn7KJuHh8Jt+jqboLjjXGu/cT76yU18r2rzFOTbcNCKiP8r33baP7prHbMA53bz6iOsPfVJR XRvAxjJntJ0s8mh9icJy0B6zvr3UrxfSKJy+4zSlSdF3OVlH4M8Xt1MSDqZVMWpqYoe1JVMfaam1 LY6QhuaXLpACEUX645FSS3jdcZz2FNHUDXleUJ30Hf3vVDsmDhG1lzaTjh1tvZcDGimR/z0aOTkp 0ZoC/YK2Kt37nx1YvIzjLVnBm8SIPHqYnSIUI71YKf09lhT23BiqDiFHSLm50ivaVP0XQEQT5zxS CoXWHcdGDc/EnZM6os/+6n8VP/x0BV6aVYXKKmdsmKTRwLgz22DaHftYuk/HeDIoSuzQkzJ0I4kd lQE4ZbT4/c+NmPVONVJThCUboGmt0uMR+KpkD4ssAxO/mkTACWJETdEgVqxZNW1lqGaFFKRex151 tJDy6lA3tHSMRaklMs49ftn4tpg4Qf16EVnO0eZWChXuhETesmmt7N93ddajtNpdpwN6pmBEbia6 dfVie7kfW7fbL9iNjdBHbr+W+nGyBRFsDx63vWsAACAASURBVOmfqntuIevE6hqewjP7mVQlRpOn lmP5iihH+EJsXVsy9a1QbQwpSH37T5oAgWGhbgh3jEUpHB3nnOvYXsOV57eDFRtdyWKSgrrV1tn/ R4aEiPZVPfqvziAXN05LfQ5IwWl5mejU0YMt2wIor7BfmNat9+mjWlpfG/SHiCZMYsZKJvW0zWB9 qR9ULidzCKgSI4qJ9cEnMRnoZK0tmfpYqNaFXEPKKyibC8hRoW6I5BivKUVCyZ5rjjksDffc2kl5 4bQ2MnNuFTY5wGiBhIj+0N9wuXXrImYApvW2eQur9T1NZuQXbx4nHJ2OO28yf/9hqHqRN/cXX68K dYqPRUHgTxeocYIcb4DGVCm6zi/M3tK8KSFHSL0GTnpYADGvfPFIqTlmZ3ymxcxbrwm5rczUCj79 WiWeea0SVQ6YeqHQ3/+7vwsorLrbElmbjh6RpW++JWG321np+tKm0VJ6qoaBikdLhw5MA0XI/ea7 BvicYYzotscHThUjAtkoRPG6kqk/Noe6lyANz9/USwp5W/MLo/3MohQtMbXXTxzfFpeOU2vSvWRZ LR5+Zifo1e405MQMkH8uFRZFVreN3PzQVGN6uoYNZfavsaxYWY9fNvpwyrFqrTIP6puqm+GvXN2I Tb+xKkXz3CkTo3+bE7peCPnz2pJpxc3btJcgHXDwDacC4tzmF8bymUUpFmrm3pOWJnD1Re2Vxy4i bwsPP70Tv2219w8HucK59rL2utFC1857Pd7mwrU4NxqV0BoLWb+tXe9DXb1963L03SaHrRlpAqr9 4tEot6IyALL+49Q6AVViNOVxMs+Pac0oRKVFw9qSqS81P7HXN7b3oJsmAji2+YWxfmZRipVc/PfR lM+F57bF6UPNd5wYXLsHn6jQg+YFH7P6PcVposXb8We1Qbcuez3WVldHaXmDDkpFwag2ujB9v6YR ZBFnRyKHrZ99XY/ynQEce4TaKdE/Hp6u7xejDbycWiYwcUJbFIw23+MGidGCYlNnPjpflP/gA8XF e8ZH2suoIa9g43JAmCZIBjo2dDBIWPOad2KG8vUicoY7Y16VrdMpZC1HfvdIkJI1vTK7Cq+9UWXr iClnXw/yz2ijIuz1Ht1Khh5Pv7rTEVabe1TMAR9IjGhfndlJgRjpVfT40X/hrJzVwfXd46ckOVSt FVX/BmD6LkkeKQVjV/ueHkrVgfTojyB5bq6qtmfaiJydXj6haR8VbTBN5kR7eMjlEfXEdz82gmIg WZ0qq6Ue/ZnqQF7RVSUa9Xfr6sGOioDt08Oq2hhLvm4TI72NQi5vvkF2D0HKOfjKgwBcHwuQSO5h UYqEUnzXkPHChflqjRfuf2yHvpM/vprGdnf/vim4dFw7XH1RO9CGUjsTTR9R8Luff/FhQ5kfASn1 zZ121YmE4PwxbdDogx5M0w5h+mZVA35c68OQE9QZPPTaLwWnDs7E9h0BikRqF27HlKtKjMizyjtL TJ2m24OZEFi7pmTaouCDe0zZ5Y0tPQ8SLwdfoOL9iNwM3HyV+ebHyR66QtUGOOMZoCm619+2xw9d v14p+nTQaUPUrocZbW3p1fDYTYEEySlp89Q2S9MNSM4bY/7USfOyWvtMDmxnvV1tS+j3lBShj2DH nBbz7pHWmqeff3lWFZ6fWRnRtYl4kSoxeuSZCt3PokpmUorFSwqzhwaXsccIqffASRcBOD74AhXv KaTx5q1+0EY7M1Oyegnvso8Hl09ohzNPVfflnzm3Gv99YaflfugO6OnVR3yTrmwPEiW7Erk9euqV nfr+KtqP01KimE7kk418wdEIhabT7EpHHpKGC85pq6+30FSelYnaTgYPZAlI9VCVaOqW9iuFDXmg qnCb8724oK0+VWt2NawQI6qzEGi/tmTqA8H133OEVFBKw6c9FCv4YrPf80gpfqL0B2/a5H3izyhM DnZEcqV1IfJArvoXdphm66dKvm/QfynG6o2a3DSRqfa4s+wfMVE/vrmgOuTIrjUO8Zy3IuAjjd4L 36pC6WZ7tx3Ewymae0mMzj/H/GfKKjEy2iok9i8qzPl112fjDb3mFWwsBUR28DHV71WJ0pz51Xjs +cQOl2yFJd3N/9ymx8pR/RwY+dOUF5luF4xWN9ozymrt9YH/7sDC98yZQ9+3iwdnjshCwSj72/XQ UxV4u6imteabep6CAF51oZrQB8EVjSYMQvB9bnqfKGKkM5cYubgw512D/64puxMn/NLRG/DcbZyw 6lXV9B1t1mvbRkOihkomx6g3XKHONxv5orvlX9vx68aWp6fMfkbox8kjd3dW7pamtXrTH2zyTk7P plmJPFiv+KYeRR/U6lN5FIPKrkRulAb8IVWfygs3/Whm/Wi9jb6LZHBxxMHqpvDyTsrAth0B/Jig xg6qxOh/L+3EnPnW/kjRny8hvlpbMnW58aztEqQDD/rrURC41Dhh5SuLUnS06aGk0BGqEnldePyF naD1EKsSmS3/5RJ1AhtJO2hK6//u367UcovMo8kwYulHTUYRqr0ctNTunH29GHx8Bsj4wMrNpt+u brJKpLJVJRJcenLJ4i+REhnKXKTAgpbEqPCtaptQiQ1rS6bONQrfJUi9Dp40EsAZxgmrX1mUIiNO bkFUrkfQw0lB9KxMNNq74jz1cZlaahO1+dZ7t+um0i1dY/ZxcpRKI4YPP6unxV0c2Nseg42DD0rV nbeSAYJViUZlbxXVIC1NA4WcUJHIBD4zQ8Pn31jXLhXtMPIkMbpkrPk/Qu0VI2qdrF9bMu0Zo527 BKn3oEkXmOkyyCggmlcWpfC0/nJJO6WL/HdOK8f8peasmYRvye6zfQ9IsSykwe5Sm96RR3ISom+/ t9YCLbgeFPPo4y/qdXGiUOoUE8nqRKO01FSBL1ZaN6Kg+FiffFkP8rWoKs4STYt2bO9ByQ+Nlo72 ze6/xBUjIiXarS2Zep/BbJcg9Rk46RoA/YwTdr2yKO1Nnhb6aUGYFsVVJLJQuu/RHVi52ro/SEY7 aPqxn8Wjg+dmkBCV61M6ofYSGXWz8pWixH74WR2+WtUACoZn9aZf8o9nbFy3st0kghSm5OjD1Kwr 0QisXRtND3hIG2ndllSJ0bPTKzFjrl3TdHv0QlrvQ/76xNpvH9Qrs0uQeg2c9A8Anfe41KYPLEq7 we/X3YsJZ6nzETb7nWrQ2okdsXbItFvFBund9PZ890JhJf7vvnJ9zcQOLwZ71ib0p81b/Hj/kzpQ yIWMdIH9e1jnFql9O49pVoWhWxf6KO2RIiexZDWqItEPnjOGZeplbCxzj1m4KjGiH2SvzrF2Wj5c v2oy8Maakmnr6RpdkJp82FVOBcQugQqXgRXnWJSAwwel4rF7OiubZ6fQ4k+/at8ud/rCDein3tqM /O7d/sB2fP51gy1eC2L5vlD8n/eW12HVj43IzBCwwl8f+Yhb9UOjLXt5SCjeXlyL9FSh7Hknwftt WwA/rbNvijbSZyH/jCxMnGD+uiqJEX0fHJWE9t6akqlfU510Aeox8KoDpMCNjqokoJvdqvDo4AaT 8NzjM3D3X9WFGievC9PftPfBJFdH7dtqyh676W9UYfK0cny8ot62EA3xNo42epJF3k/rfPo2hpxu akdM6ekCxcvNinkTXeuNdSUKRKgqIi15hSfvEbSu5NREYnTlBUkiRmTWAJSsLZm6lPpDf7r98PcF 9nDa4Ji+MmJwmD21Y0QSdeLmWfJQoNJb9z8p6qNNf3SMB8vrgbJf/TPnVYOmIrdud8/0jMGlpdeP Pq8D/acfKqOHZYJc5qhIJx6Trlv92bm29uTLO1G+w6/kjzIxu+L8droFnhN94CWbGFF/aEAf41nW BUlqYn9h3ZYTo+yIX5NJlFQ9kAbsa2/fhlU/WG+8YJRvvHbsYP7sMDkSJQ8dm7YkjhAZvIzX4o9q Qf8piuqo4Vkg7+dmp306emwXc9oXQ/14x40dzW6enh+53UlPE3oIFSUFxJCpqu8+OaB13DRdEB8p sb/xUf+r0HvQjWcC4hTjoBNfk2FNidZUVMwbU3+SJR25VdlQZp73gXieE/pjkD/KHF9cNBp64L8V IH9zZLGVDIlCXsxfUqOviZgdnHDRB7Uod4BFGnkJ+fDzemiK9mmRWTgZcnz6pf17lc4ckYmrLzZ/ Y7grvKELYG3J1Ifoe6uPkITUekp9b7Ozv8qJPFJS5RKEetSJfv0aTJjCp82VJLQ//2JCZs5+9Fus 3btLa7CguAan5WXihsvN/4PWYsEWnfh5XaPuZd7nB+iPttmJ8iTBe+H1Sj3on9n5R5IfRTy+5lLz +84VYtQEKCc3V3qLi4VPHyEdMOCma4RA70jg2X1NIo6UKKYJuc5RkWio/uQr9lnStdQm2ogZT7hl cm1EFkO0sZQTdN9t5GFjZ6XEMYfHt6eHhN4JIySjX/1+6KMYVcYOtFcpI03Tpwgrdlr7PJEYXXtZ UosRdbOW0rby6Z+/nbZTN3ESQvY0Ot8NrzRSomiGZicydCDLLyuT7groTDVi9NQrlfofbSvbw2XZ R4AMEd54txpTHt9hXyUUlkzGDuRdQ0UaNTwTz0ztAnKlZFVSJUZkPetEg41wXGUA+9F5w+a2R7iL nXhu3qIaUOwOs5OVokTid+4Zarwv/PupCpCTVE5MIJEIvPZGFR58wvzvvcHooTv3gdlrckbewa8q xcjOvYXBbYzmfUBqetgjbdSoUpqYNX9yNpraxHgthUhwqyjRnLFheh5j81u87Z8PlevOK1u8gE8w ARcToHUzitOlKt11c0cMO1mN1wiqM4VZUTFNRyMjN4oRMZEIdKNXrTrTY2lAPrMfIpWiRNNpZidy B3PdZe2VLNBSXW+6axve+9iejY1ms+L8mEBLBChkxgXX/qYbtbR0TTzHb/lzB9AoxuykKiCpm8WI GAsNXejVK4Xs7OQ9SJE8ECRKlMz+1WFMpz3xkjmRZ7MyBSaObwearzY7kbXZ7Hersd7CgHpmt4Hz YwLRECjb7Mdjz1cgINVY4NHfE/JcMdMkJ6QsRmF6V4ocOuvVAgFXj5CMJjpdlLp29mD8mWqcpJIY Pf3aTlBUUk5MIF4CNbXWWprFU18yB//PsxXw+SQorpbZieJ0kfd18vsYT1IlRq+/Ve3aabpmPHXH 3l4BdEmUP2NOFaVuXTwYO1qNGJFVFXnr5sQEzCIg3aNHu5r8+Is7dR91tLnc7ERRWj2aiNlyjdw9 me36jNpIYmTW7I3ZzKLPT+xL93ghRQe4fc4uqPVOE6Xe+6XgySlqonok1gMZ1In8lgnEQID2pdXU Slx+nvmRVcnVkNeLqEcjpxybjr9f1yGG1oS/JfG++1KHpEnR9CZ88911lkRJhfUdrSlFY+jQr5c6 MaJFzMT5deSu54tr61wCtNVh2pNqzMJpI3c0338So9tvMN8XX+KJkf486aA0IPEEiZpntyhRBM7H 71MzMiKXIG4173TunzKuWaIQeGdxDf4xpVxJcyL9UapKjMgNWIL+EG0aIQkI8yVcyaMQfaZ2iRIF 1vv3nftEX+EI7qBpCbftwo6gWXwJEzCVAIXquO4favYqkSiF8+hysqKRkRN9UprYaem5F69NJ08N 5jtSMrGW8WZltSgdc1gaptyuRoz+99JOR7uRj7ev+H4mYCaBku8bcOmNW5TsVaJN7aEcotL3/x8K pukSXIyaur06q4NXAubv/jTzqTIhLxIlSqr3KdHDeM+taqK8PvxMhZIvlgl4OQsm4FgCFMLi+cIm /3dm7/8zPIXTd5OSqu9/UogRgFQEMryQSHdosFhTH3LVovTFynplYnT/Yzuw6P1aU3lwZkwgWQiQ B+9HnlWzgdYQueUr6pR8/5NFjOhZDABtvBCIz1e9i55qlaJkeHUwG8fkqeVY9im7AjKbK+eXXATI EzptoA0EpOk+JEmUDGEyk+qbC2rw2PPJs8fQ70EmBehL6DWk5g+IKlFqXo4ZnynC66df2R/N0oy2 cB5MwAkE6A88xVdS9QPSrDaSGJGAJlMSgUAaCVLSjJCMznWDKN04eRu++a7BqDK/MgEmYBIBMpuu b5BQ4dXBjComoxgRN02KtmRll24GRLflQaLk1OHw9XewGLnteeL6uosAbZ+g/XxOS8kqRkY/0Agp aRMtGHo9wJUKwkzECvWKm7diza+Nsd7O9zEBJhAhAdrP1+iTuGSs+a6GIqzCHpeRk+Rkm6YLBiAR aJPUgkQwCt+q1pnYLUr0ML65sBrr1vuC+4jfMwEmoJDAK7Or0NAgbf9RSt9/w3xcYXMdnbUAPCRI zvh5YCMqu0WJHsbpc6uweYvfRgpcNBNITgL0/acwFuG8L6gkw2K0my4JEq0jJX2yS5ToYXx5dhW2 lbMYJf1DyABsI0DT936/NH3zfGsNYjHak1DST9kF47BalOhh5MB6wT3A75mAfQTI0Ims71TELgrV Khajvanw6KgZExIl8hmnOrEYqSbM+TOB6AksKK7FPx9S4yk8uDb0/f/fy+r/zgSX6Yb3JEgujA+p Fq1qUWIxUtt/nDsTiIfAex/XKRUl4/tfV58osbrjob3nvSRITZ4H9zye1J+yMgVy9lU3m0luRkYO yUxqxtx4JuBkAocMUOcvgL7/+WeYH2rdyTwjrZu6v7qR1sBh15EYTRzfTolvquCmUuRJEWR2Hnwu Gd5npFPrOTEB5xG47rL2yr//FBKdEsc2293/EvCzIO3mAavEyCiS9j4JAcyc17QXyjieDK8eXr1M hm52VRszMwQun6D+x6gBhUXJINH0KqBV0Z8FdiUNWC5GRldccX47x/rUMurIr0wg0Ql0bK9ZKkYG TxKliwuSfiuogQM0Qkp6d9JWj4x20f/9DbkuSfEKHr43BxPmcy0vCIehw6eiIdCtiwdjR7dRPk3X Up14pNREJiBkJQlScvk4b/ZU0DDdijWjZsXu9ZEeSq8XePpVtjHZC06IA7SJkRMTiJdA7/1S8OSU zvFmE/f99P0nv3rkyihZk9S0eg0yuUdIVs4Zt/agjTuzDSZO4OF7a5z4PBMwg0D/fs4QI6MtNFPi 1JAYRh1Vvnr8qNEgkncNyQprmmg7kESJLPA4MQG7CGRkJL4F5CEDUvGfu+0fGTXv42QWJQ2o0gSQ lNuFnShGxsNJ0SztcvRo1IFf3UugPs64jhr9VUjgdPxR6Zh2xz6ObWGyilIDtNqkXENyshgZ35Kz R2aB/jAkc3wUgwW/RkegsZHX11oiNuzkDNzy5w4tnXbMcRIlSkm1ppRVvUOTkOodNzmmmwFVYvT6 73GVzGzqmSMy9fqmpyX2L1YzmXFeTKAlAqOHZyoTI4pAa3YiUco/I8vsbJ2aX13x873qvIDY4dQa ml0vVWJ0273b8elXTdbzNN1mZiI3I5QK36pC6WYOUWEmW84reQgUjMoC7flTkcgZK/m/o2SMbMwq xwgcakQiMCtfB+aj65AmZHIIkhVi9MRLO6FipESi9OIjXXFQ3xQHPkdcJSbgbAIXntvGEjGi6bWX Z5lvtk2ilAQjJX2mToOQCT9CskKMjK8kiZIqV0CP/qszjj5MndNHow38ygQShQBto7gwX81Witsf 2D0yMniRbzoWJYNGNK9NAyNNAluiuc1t11opRgabJ1/eielvmv9LifK/99ZOGHJChlEUvzIBJtAC Ado+QdsoVKS/3bMdy1eE9rqmUpTMXhJQwSa2POVmuk8LaFpZbBk4/y47xMigQh4XVPxSovxvu7YD Rg3j8BUGa35lAs0JXHNpe6j64339Hdvw+dfhPa6pEiUSWTLOSMC0ldrkFVLobxKtgXaKkcGSHkpy B2L2Qiflf93E9qANjDPnJp+ncIMvvzKB5gQ6ddBwwTltlfmlu/KWrfh5XWPzYkN+pu8/JcNXXciL Yjh47WXt9bso5HrCJCFLqS3erBp/WU2CzQA5QYyMB4UWOn0+4PLzzJ/HvuK8dsjK0KDC5NSoP78y AbcQ+EOfFDx2jxrvCxTldfa71Vi/0RcVDhIl8lFp9tRhoomSDDQtHWnz5uWQzCaM1DpJjIwnd8bc KvzvJTUOMcj31VUXqjFnNerPr0zA6QTI+4JKMZo+typqMTKY0fS9ijVlEqVEmb4T0DYRLyNM2gYD nptfad7Y2LdjZjuC9xnFmi/tI3jkGTWO1c85PQvU9pQU3kAba//wfe4lcFpeJu66uaOSBtDI6OnX dmLzlvj2AKoUpTOGun9NSRMB3ZZBjxgrpVgvhDxQSY9alCn9QSbPBmanux4q37XpNd68ac63vkHi 5qvMd11CbScXZLPnV2N9aXTTCvG2i+9nAnYRoDhGKqbDqT1vLqjBo89VQJrkickILWP29N31l7fX 16oXFNfa1Q1xlys0/EqZ6CMkoQXWx52jjRmoEiPagf3+7zuwzWoePTQkcioSjQ6fe6gLDh/Ee5VU 8OU8nUWAjIVUidGc+dW6H0mzxMggp2qkRD9yR+S61hjA37ApRx8hNU3ZSbhWkFSKkeEOxHiYzHol kbv5n9vNym6vfKbc3glDT3Ltw7lXe/gAE2hO4OqL2imLHUTeVh57Xs2aL7WDREmFRxcXi1JpcbHQ p3V0QZJC6MOl5p3u9M9uFCOD6Zff1uNPt2zFO4vV2JP87S8dcM5p5vrVM+ruhFdaxD7qEB4JOqEv rKxD504e3eHwGEXP9qtzqkDeVlQnVW7GXClKQQMifQ0JkL8C7loQd7MYGQ/7T+sa8eLrVfD7ocQY 46qL2qFDew3PvGa+J2KjDXa9nnB0Ouh/8Ue1mLuoBt+sijMIkF0NMbFcjwf6jxBVTkRNrGpMWR06 IBVTFcYxou0TVoZ7MITP7A28JEqNPmDJMpesKQUNiHRB8gY8P/pFIKaHxI6bEkGMDG5bt/vx3xeb fpGpsBAcf1YbZKQLPPqc+l99RpusfM09PgP0f+F7tZi3sBrf/RTZpkUr66i6LCEAGjEksvk/rY/Q H1pVibZl2OFRW5Uo3XZNB/h9cpcXclXczMlXrjHy8dCbIwZOqawVlbcCQv9snHTiayKJkcE3EAA+ +bIebdto6N8v1Ths2utBfVPRsb0Hm7b4UbHTGT88AhIgsTQr9TkgBWT+26mjB1u2BVBe4Yx2mtW+ UPmQEJ11ahZ0p7uHmjd9+eaCauxwyHNC7R53Vhtcc0mTd4JQHOI99u+nKvDGu2qmziOp2+ff1CMz Q8OAA8397p9yXAZ+2eDT/0dSD7uuEZDPrymZ9iWVrwvQqlV3yt4DbzofgHPj+gL6XhsVpt3B8Uzs 6hQq97Ov1DyYlDftYj9zRBbW/OrDr1HuNlfBhKYUzj2jjel7pw7snaL7+WvXVmsS4MrEFKbTh2bi v/d2xjGHmydERj/TNHJdvUm2zkamMb6SN5ILzjXvh0vzakyeWo6iD+yf2iJRUvGD1BWipImpa7+d qtsx7BoR9Rk4aSSAfs07zCmfE3FkFIotPZgej8Ah/c39tWSURdNbldUSqx0wtdW/Xwp65vy+jGlU 0KRXGhWSAGdladj0mx87qxJDmIafkoHrr1DnXLeqOoDnZ6rxVB9N12ZmCPzpwvagTd+q0rW3b8OK b8I7SVVVdqh86QdpMoqSP+C9bd2qKfpDt0uQeg2adAyAY0OBsvtYsoiRwfmrkgb9F+qRiqzIjjks TRc9KsfOJCFw8rHpSqswoF+qPq2Vnq5hQ5kP1TXO+OUfbaPph8S1l7bX14q6dNr1tY02m1avX/5F vel771ottNkFBx+UiosK2mLkYPM3ulNR5H2BRkY0neW0pFKUflzr078DDmvzzqWF3f5u1GnXk917 0KT9AZxhnHDKa7KJkcG95IdG/LY1gOOPVvMHm0ZgNK21YmW9aTvRjbpH+kpThxeca77T2VDlD/xD qm6BlpoisHa9zzFTUqHqGnzsxGPS8eeLm+L67Ntl19c1+BJT39Pifumm+NzkxFMhcoMzeVJH9NpP TXRk8r7wyLMVqKl17g8TVaJEcdRoZmSjjf0b4tn4Zm3J1KeM47ue8D4DJmVA4FLjhBNek1WMDPZk Fk4PUJ6iTa40rUW/trduD2BbufVTWrQLnkYsVkbBHXRQKgpGNa1d/bCmEY0ONcr74xFpuPrC9nro gpx91UxrGs+Z8frdj436pk3js9Wv5HlBpck6bUb97wvusDZVJUr0t8RhorR4bcnUN4xnbZcg9Rt4 fVVAaLcYJ+x+/csl7fSpFrPr4RQDhkjbRb9mPvi0DgJCN0yI9L5Ir+vbKwW0QL55qx8/r7N+CoP+ CLZv58FBfdX8Im6JA00LkZUfrddRHWgvmBPSUYem4U8XtsPFBW3RI9saITLa/ez0Sqz5xfpngJwC k+cF+qGgKlGwzKddth+PREnFd8NJoiQhX1tbMu1Do993CdLPqx6q7T3wxisBYc0cilGDEK80RXH2 SPMXM+9/bAeWfBg67HCIajjm0I6KAFaubkCbTE2JKFFDaZMppa9t2GBK0Tc7tPMoa1u4jqSpSwrh QRM4q35oBJng25EOGZCKyye0xcQJ7ZQZeoRr18x51SicZ32wx4EHpuLisW11k/1w9Yvn3JMv78Qr c+w31IilDZ9+mdiipEnt32tWTf3RYLNLkOhArwE3jxQCvY2TdryqEqMpj+/QN0/a0SYzyiQzaZV7 laiOhw5M09eVSJSsHDHQ1N2n9GuwrfUjJaNvDhuYpk+PEWea0rBKmPr3TcGl49rpI4QDelo7SjTa To5En/h9c7ZxzIrXkUOawkb03l9du+/9zw68VWTfHiMzOKoUpa9WNcQdWiOeNgrg1jWrpu6Ky7OH IPUeNOkQAMfHU0A896oUIze7Zg9mSsN4r1fgYEVm4bSuRCOGb75r0PfxBJet+j198eoaJFRZF0ZS /yMObhKm2jqJ739uVGbw0Wf/FN2SjEIH0KZeuxK5ynnyFetdS106ri2uPF9tYMnrbt+m/4izi62Z 5aoSpRG5mfji2wb8ttWWOettiwtzCjVkfQAAIABJREFU/i+Y056CNHBSVwBjgi+w6j2LUeSkv/y2 AVWKjQGGn5Jpy36lku8b8frb1boQqBLdSEjTWs4F57RFTZ3U15giuSeSa2jf1QXntMHNV3cAbeK1 Ky1fUYfHX9iJtxU5922pXZ06aLj8vHYw239bcHlk1v23e7Y70cQ5uJpRv1clSqcOtkeUpBTL166a +mIwiD0Eqc+gSWRz9OfgC6x4z2IUPWVaiCevC7Q/RVWi/UoZGZrlmwdp2oxEd96iGgT8AFnG2ZVI mC48ty12VjWNmGKtR86+Hpx/Tlvcek0HJe6hIq0X+fp7bnqlbk1ntfkvmbBTmHHyGqIqGaEjGhqc a9YdT9tJlMgNmNkM7RAlTcjZa0qmLQrmsYcgHTngwfJaUXUTAHVPTHDppH6KDBhozShRpumaIdv1 kfbxqLTAo4Jo0blrZ3tMw8l9DU0nLHivVl/Tor1EdiVy0UPCtG1HAD+uidxWvMs+TUL09+s7mu6r LFoW0/5XgUeeqcDPNljSnX9OG9D0pMr0YmElnplu/fSjyjaFypvWkhNBlITEY2tWTVsZ3Ma9Yk7k FWxcDghLPDawGAV3RezvyckmsSRHmyoT/TGjMOx2pe7ZXowelqnUnUwkbaNRBnkWX/R+bYtrTB3b a/pGXHIManci56E0NWd29NNI2kWbry/KbwsVPiiDy3/46Qp9RB18LNHfX3dZeyVha668ZSt+Xhf5 j65YOQuBAUUzcr4Lvn9vQRpb+m9IXBd8kYr3LEbmU504vq3uGdn8nHfnSFMihsv83Uetfdeze5Mw qdgaEG1LSJRopEpGEB4N6NhBQ98DUkBTfXYnCjlCnrvtECJqO7mF+scNHZVj+L/7tieM8UK0sFSJ 0hU3b8WaX5WKUsVJA7I7TZ68Z9yjEIK0cRykeC1aMNFcz2IUDa3orqXF4j9doNZ6iUK70wjBbl94 ZKlGMaTI3Qyn3QTI/Y9hGLL7qLXvaGMvTdOpTGS88PLsKmwrt8VCTGXTosrbpaK0aPHMnOHNG7rH GhKd7HPQLTXQpLIREotR8y4w9zNt7ly3wQdyO68qHdDDC7LCa2gEvv3ePgetFPPo4y/q9bAdZApv p/m0KtbR5EuRgW+9dzu+/V7pL9uwVdqvu1f3MpE/Su30MW3kJTdANDJN9kTfQRWb5unH3rJP69XE FhPylbUl04qb991eIyS6YEhB6W8C6NL84ng/sxjFSzDy+7t38+Lc07OUzDEH14KcVU5/swpbttn/ K5W8HdAak0rLw+C2O+X9C4WVeO2Navh89v5xpj9g9GtddbIruqvqdsWTf1amwMTx7ZR835VM3wlt 1OIZ3d5q3ua9Rkh0Qe+BN51EMd2aXxzPZxajeOhFf29lVUCfV8/KND8SZXBtyAcdCd9v2wIgZ7B2 ps1b/Hj/kzqQp3QK206/1hM5kX82GhF9sbLBMs8SoXimpQndKSo5R1Wd/jGlPOGtZ2NhSE6CVY6U Fr5Xq+99jKVuoe5Jlbj+p1VT97KQakGQbuwJiGGhMorlGDlOHHOa+UP4ZDDtjoV38D3kJ67BB5AH ApXp+KPS9TDMX5XUg8KT25nKNvtRvLwOZA1Hgd5UBQG0q43kXeH2B7brU5VWungK1V4ajf7v/s6g uFMqE60X3fVQue7WSWU5bs5bpSileIW+FGBSPLHvFhbmTAnFOqQg9Rn0Vz8gJ4a6IdpjtMCuIuoj mbLOX2J/6OFoedhx/berG/S9J4MVbqKldg04MFXf/PnLRp8jgp9RXJ+lH9Xhp3U+PRJnTjd3j5im v1GFO6eVY/mKekeEzZg4oa3ug0/1M134VjUee36na4MrquYTnD+JEq0jZ2WY64iZNuKmmidKs9eW TN1ruo7aEVKQ9usyZZOWVTUJQFw/e0iMVLgIof0wtIufU+QE1pf68M6SWqSlqgljEVyTU47NAE3j kLcFJyRqe9EHtfi11I8O7TRYEejOzHbTAv49/9mBDz6pc0RgwWOPSMNVF7XHqbnqrRtpi8FLr7vT U7eZz0A0eTU0Sn00QwJipkcHs0RJSPlQ8w2xRvtCCtK6dXcG+gyYdAoE+hgXRvuqUozs3JwZLQcn XU9RMmmXN8WfoXhAKtOgP6Tqng0oTLRTQkWvW+/TvT5s2uJHp44ePTihSgbx5j3r7Wrc/98KFH9U 65jRAe11u3Zie0tiNd3yr+1Y/AHPgsTyHNHUGlnbOlGU/H55/brvpoWMlBhSkAjAAQMn7S8EhsQC g8UoFmrW3UMjF4oSe9xRasKjB7eEzM8zMzR8/Z29C+/BdSLXOfOX1FjGILjsSN6/8W41pjxeoY/q yDjFCYnWCGktmLxDq07U/kl3bQeNbDnFTkC1KJERRdQRlwXWLH29+90ttapFQeo98AYfhLispRtb Os5i1BIZZx3/cW2jbpGmKhJtcGv1taUxbWyLShtcl+D3xOClWbSxMoDjjlQvzsFlh3pPcXumPlGh W5FV7HSGENH+LgoaSBGcaSuB6vTcjErQfiq7jTVUt9Oq/FWKEu19ilqUBApbWj8iJi0K0lEDp5bV iqprAUT8TWUxsuoxM6ecHTubTMPT0zRY4biUotKSb7MNZT49tIU5rYg/F3KWSsK0s1KCnKhancik lox0SJBos69T0ojcDDx+b2dLng1qM5l0v2NxOAynsFZZDxKlX0t9GGNyFG5aU4pWlATEA2tKppa0 1N6QG2ONi/MKSmcDONv4HO6VxSgcHeefOy0vEzdeoX5To0Hi6dcqQVZjTkvkqJb2VV2p2P0StZvW huYuqsE3NoSND8edfAWeOTxTubNeow40RUcjI/rDyUkdgd77peDJKZ1NL4BM8p9+LSIryIBX+rss KOy5vaVKtDhCohv6DLipEwROb+lm4ziLkUHCva80fbXkwzp4NHMtc1oiQvuiaCqPDC2ctlZAZrOv zKlCQ4Oa/VsffV6nmzFPf7Pa1vDRofrmgnPb4I4bO4IiB1uR6IfJs9Mro1+LsKJyCVYGjb7JFRB5 1DAzGSOllasbQLHMwqTPFxX2eCTMeYQdIeXmr+/rEZ4fw2XAYhSOjjvP0Y57CmNuVZozv1p3Bkqe FpyWPB6A9m+dPjQzbstEmpKjX5M//2KvR4tQjIeckKH/oVJtfWmU7RQHvUZ9kulV5Ujp4WcqwqG8 Z/HMPUOWN784rCDRxXkFpd8DOLD5jfSZxSgUlcQ4RtE9J09SHzogmBYFWCPvzQHnLKMEVw8d2ms4 8uA0HH5wKg7okYIe2R60ydL2uMb4QKbltFb2/U+NesTdVT80wOc8vdVDZYwengmasrUq0b6qJ18O afVrVRWSvhw7RElInFRUmLMsHPxIBOlBALRJdo/EYrQHjoT8QPuVaDf+OQrcPoUDZncgwHB1a36O XBNlpGtITxfw+yRq6qTugbqx0dnrISSk487MwrgzrRsJE7up/yMPK7ypvflzZMfnfr1S8Ph9ataU QoyUtneS2V0LC0XYn2Vh15AIUu+BN9H8wkXBwFiMgmkk7nsaqZAvvPKdARx7RMTGlnED+eMR6Ti4 f6rulYCC3zk50Zw5hUCg/UJVNRL1DdKxIzziqGnAhLPb4P7/64RBijdHB/cbGS7c8eAOfPOdM7x3 BNctWd9v3xHAF9824NTB5o6OaU2JQqzTJvygNOutwnazgj6HfNuqIHU64cGN6XW7zb9ZjEJyTOiD P/zciIXv18LjESDv3lak7K5e5B6Xgb69UnQT8dLNYX9YWVElV5dB1oMU4v4/d3fG4YOsNW0n9z/P z6zi2EUOfIJ+2+q3RJSkEHevLZm6qjUErU7ZUQZDC0pnSiCfpm9UDPEp0Nbsd6pbqyufdwABKyLS hmqmU02kQ9XVScdIiGh96IbLrTPpN9pPBhxvLqwGuWzi5GwCNFr+9537mF5Jipf2n2d3NAqZ0aWo sFNYiwcqPCJByhu7cdzE8e1eUyFGHGzL9GdAeYb79/DirBHqg/+FasiyT+t0x7orvtljOiDUpUl9 jIRoxCmZuOkq64WIwD/1SiVmzHXePrOkfihaabwqUVr6Ue36ISdk7tdK8frpiASp9LfGU7O7eOdH kmE017AYRUPLeddSWJGrLmxnS8U+/6ZeN6H+8LM6W8p3aqFkpj5qWJbu6seOOvKoyA7q5pV5+KBU TLnd/JESgH5CiJ9aq2lEgiSlfA3AuNYyi+Y8i1E0tJx7bbcuHj3ECK1P2JHW/NqoC9O7xbVwumWb Sj5tszR9H9Gl49RHbW2pHbRW9PpbPPXeEh+3HD/msDTcc2sns6vbTQixubVMIxWkCwG80FpmkZ5n MYqUlHuus9r1UCgy09+swluLakB7gJIl0X4S2nk/api5llLR8CMLOgqi58SNzdG0g6/dTcBkUSoW QgzenXvL7yIVpA4AylvOJvIzLEaRs3LblakpAhcXtEXBaHtGSwavT76ox4L3an6PrOrs/UBGnaN5 peCHucel47QhmZY5Pm2pfryvqCUy7j9uoihdI4R4NBIiEQkSZSSlfBPA6EgybekaFqOWyCTW8aMO SdN/tZN3b7sTWfkUfVCD1T81QrpYm8hI4ZD+qcg7McNSrwot9R+NRskHnVO9arRUbz4eHQGTRClb CLEpkpKjEaSxAKZHkmmoa1iMQlFJ7GMFo7Jwxfn2GD2EIkvrGxQGfNWPDa4QJxKhAf1SccIx6SCW TkhLljV5KP92NW9wdUJ/WFEH+mF5500xuxFbJIQYHmk9oxEkmqSOacWSxSjS7ki869q20XDe2W10 wwcnta7og1p8+mU9Vqysh1OC4RGfrEyBwwam4Y9HpOlTck5i9uhzO0HrRZySj8Apx6bj9htiEqVL hBDPR0osYkGiDKWULwK4INLM6ToWo2hoJe61hw5IxejhWTjlOPun8ZpTXrfBh5XfNej/v1/TiNJN PstGUB3bayBXKxQgkTwoWOUJozmDcJ9fnVOFFwo5ims4RslwLkZRai+EiNiTbrSCdCqAiPcjsRgl w2MaXRuHnZyBW/5MNjLOTuRzjTwMbNzkQ+kmP0o3+0ARdmMZTZGT2k4dNHTu6EGPHA96ZnuxXw+v 7mm7a+dWvXfZBmreoqZwGWRaz4kJEIEoRalQCFEQDbloBYm+PRH5AWExiqYbkutaWhspGNUGl59n 356ZWIn7/UBFZUDf80SOVBt9TZYSXo8Atcv4nJmu6Y5MMzMFaI+QmxJZKc5dVA165cQEmhOIQpRO F0K80/z+cJ+jEiTKSEp5L4C/hcuUxSgcHT5nEKCRw4Sz2oCilHJyBoEHn6jAu0s5PIQzesO5tYhw psMrRPhwE81bGIsg/QHA6uYZGZ/Zh5VBgl8jJdC+rYb8M7Iw7iwWpkiZmX0dGSy8uaDasrUzs+vP +VlPYERuBm6+qsXp9/uEELdGW6uoBYkKkFJ+AODE5oU9N6MSr8xmh4rNufDnyAjQesroYZksTJHh MuUqms14/W0WIlNgJmEmYUTpICEERRuPKsUqSJcCeCa4JBajYBr8Ph4CZHlGUWp5xBQPxfD3khDN nl8NWhPjxATiIRBClJYJIU6KJc9YBYl26e0aCrEYxYKe72mNAIXZPuvUTN0dUWvX8vnICDz8dAXe XlzDHhYiw8VXRUigmSidJ4R4NcJb97gsJkGiHKSU1wJ4+LHnd2LOfN4stwdV/mAqAa9X4PS8DFxz qT2xfUxtjA2Zfb2qAXMXVuP9j+t4jcgG/slSZJssbdurj+57aVaWmBtrm2MWJCrw7oe3n7j0wzpa T+LEBJQTILPqIw9Jw+jhmTj+KOdtsFUOIMoCFhTXYt6iat2PX5S38uVMIGoCAuL+opnZYS2wW8s0 LkGizPPyS5dCILe1gvg8EzCTABlAnDE0ExPOZsu85lzJ6ek7S2qwoyLQ/BR/ZgKqCASE39OvaNa+ a+IpwARBKjsXQhbGUwm+lwnESsDrAY45PB3DT8nAicck76jpvY/r9FhQX5XU87RcrA8T3xczAQEx t2hm9pkxZ/D7jXELUm6u9Hq6lv0MIKKY6fFWmO9nAi0RoP1Mg0/I0GMFDTootaXLEub4N6sasPjD Wn1tqLKKR0MJ07EubIgATi2ambMg3qrHLUhUgSEFZTcJyCnxVobvZwJmEdinowcn/TEdJx+brscR Mitfu/MhH3sffV6H95bXYcs2ttm2uz+4fJ3AqsUzswcBIu6IY6YIUu5Zazt4UtPWA+AJfX5CHUeA zMePPCQVfzw8HUcflgba5+SmRNNxH6+ow8df1INHQm7queSoqwCuKJqZ85QZrTVFkKgiQwrKHhGQ 15hRKc6DCagk0LO7F4f2T8XB/VP10VOXfZzjcZsct67+sRG0FvTFtw1Y84u7I92q7EfO2xEEttZI /37LC3vWmlEb0wRpeP6mXn4R+AGA14yKcR5MwCoC7dtpOLB3CvockIJ+vVLQM8eD7vt6kZZm2tcj ZFPKNvvx60Yfftnow/c/NYBiMW36jafhQsLig84kIMUdiwuz7zKrcqZ+4/IKSl8GcJ5ZleN8mICd BMhIonMnD9q1FaBpP4p+2yZT6EKV4hWgDbspv//8CgRoszjg+X2wVVsndW8IdQ0S9fUSOysD+nTb zqqAbo69dbsfPtYeO7uXy46fQJVX+vdfUNhze/xZNeVg7mjG438Afs8EAKYKnVmN5XyYQDQEaPqM /nNiAkxgbwISeMpMMaISTF3dXfxaz29EFBFl924iH2ECTIAJMAEXEGjwavIhs+tpqiBR5aSG+8yu JOfHBJgAE2ACziEgIF5ZOL07WVabmkwXpMXTcz4A5BJTa8mZMQEmwASYgFMI+ODX7lZRGdMFiSop Ie5UUVnOkwkwASbABGwmIOTL8fqsa6kFSgRpycyc9wEsbalQPs4EmAATYAKuJODzBwL/UlVzJYJE lZXAZFWV5nyZABNgAkzAFgKvFBf2/ElVycoE6fdR0iJVFed8mQATYAJMwFICDR6pKV2OUSZIhEnT cFvTYMlSaFwYE2ACTIAJmE5APrWwsNta07MNylCpIC2anvM5gFlB5fFbJsAEmAATcB+BGq8vVdna kYFDqSBRIR4/bgfgMwrkVybABJgAE3AXASnwyILZXcpU11q5IC2clbMaAi+obgjnzwSYABNgAkoI bGvwND6gJOdmmSoXJCrP25hCo6TqZmXzRybABJgAE3A4ASlw97JX9y+3opqWCBIN9SQER5S1oke5 DCbABJiAWQQEfqxon/2YWdm1lo8lgkSVyKqlEOeitLUK8XkmwASYABNwBgEZwC0rnhSNVtXGMkGa Ny+nRorA361qGJfDBJgAE2ACsROQwHtLCnPmxJ5D9HdaJkhUtZP757wACTIF58QEmAATYALOJeD3 yMD1VlfP8kB6eWPLjoaUH5sdi8lqcFweE2ACTCBRCUghH18yo/vVVrfP0hESNW7xjOzPADxrdUO5 PCbABJgAE2idgAS2BOobyMuO5clyQaIWpkpBjd1heWu5QCbABJgAEwhPQOC24jd62fL32RZBml+Y vQVC/F94KnyWCTABJsAErCQgpPzk5P7Zts1g2SJIBPik/t2eAPS1JCt5c1lMgAkwASYQmoDPr8kr J08WgdCn1R+13KghuEmDx244VJMaWd15g4/zeybABJgAE7CYgJBTFs/o/leLS92jONtGSFSLpTN6 fA2IaXvUiD8wASbABJiA1QTWeVI9SmMdRdIgWwWJKphZKwmC0hgbkYDga5gAE2ACyUpACnH1wpe6 2e5v1HZBIg8Omha4nAP5JetXgdvNBJiAzQReXjIje77NddCLt12QqBaLpvdYDOBJJwDhOjABJsAE kojAJq/0X+eU9jpCkAhGXV3KzQB+dQoYrgcTYAJMINEJSImrFxT23O6UdjpGkD6c26VSCslTd055 MrgeTIAJJDgBMd1q56mtAXWMIFFFl8zovhCQj7dWaT7PBJgAE2AC8RAQpV7p+3M8Oai411GCRA30 pHnIDv4HFY3lPJkAE2ACTABSQF7qpKk6o08cJ0hkehjQtAsB+IxK8isTYAJMgAmYRUD+t2hmzgKz cjMzH8cJEjVu6fRun0DIe8xsKOfFBJgAE0h2AlJgdWatsNUbQ7g+cKQgUYX9m3P+CYmPwlWezzEB JsAEmEDEBBoAnEd7PyO+w+ILHStIxcXCJ/2YIIByi5lwcUyACTCBxCMg8dclM3K+cHLDHCtIBG3J 7JxfpBRXOBkg140JMAEm4AICby8uzH7E6fV0tCARvMWF2a8L4Amng+T6MQEmwAScSUCU+j24GBDS mfXbXSvHCxJVtVr6b4TEl7urze+YABNgAkwgAgI+aHJc8Ws5WyO41vZLXCFIywt71krNkw+gwnZi XAEmwASYgEsISIhbF0/P+cAl1YUrBIlgLpmx789CyIvYK7hbHi2uJxNgAjYTmLNkZrepNtchquJd I0jUqqIZ3d+EkA9G1UK+mAkwASaQbAQEfhQy/RI3rBsFd42rBIkq3imQc6uUgsJVcGICTIAJMIG9 CVSLgHZOUWEn1y1xuE6QCguFPw0YD4Ff9u4HPsIEmAATSGoCUgAXFRV2W+lGCq4TJII8vzB7SwCB MwE4dsexGx8GrjMTYALuJiCBfxXNzJnl1la4UpAI9tIZPb6WQlzMRg5uffS43kyACZhKQMq3Th6Q fYepeVqcmbC4PNOLyysoux2Qd5meMWfIBJgAE3APgZVCpp/kxnWjYMSuFyRAiryCspfIaWBww/g9 E2ACTCAZCEhgS8AXOKJ4do8Nbm+va6fsdoMXspMsvxSQH+8+xu+YABNgAklBoEZq2qhEECPqrQQY ITU9dCPzy7o0CnwkIfsmxWPIjWQCTCDZCUgJed6Smd1fSxQQCTBCauoKsrzzSd9IGr4mSudwO5gA E2ACLREQErclkhhROxNmhGR02uBxm/6oBQJLAGQax/iVCTABJpBIBKSQjy+Z0f3qRGoTtSVhRkhG x1D484CU4ynorHGMX5kAE2ACCUTglX0COdckUHt2NSXhBIlatrSw+1wJ/IX3KO3qZ37DBJhAQhCQ S8iIizzWJERzmjUi4absgts3NL/0b1Lg3uBj/J4JMAEm4FICnwqZPtzte43CsU/IEZLR4KLCnPsE xP3GZ35lAkyACbiUwKpUKc5IZDGifknoEVLTg6dvnH0cwJUufRC52kyACSQzAYFf/I2BExNlr1G4 rkzoEVJTw2njbPafAbwSDgSfYwJMgAk4kMAGf8A/NBnEiNgnwQip6RHLz5eebaL0VQFR4MCHjqvE BJgAE2hGQJZByNzFM3r80OxEwn5MGkGiHjzyCpnSvrxsuhAYk7A9yg1jAkzA9QRog78mcErRjJzv XN+YKBqQBFN2u2mseFI07oPy8QJi7u6j/I4JMAEm4BwCuhhJLS/ZxIh6IKkEiRpcWDiwoaPcni8l ZjvnEeSaMAEmwAR0ApulRwx2a8TXePswqabsgmHl5kqvZ99NL0PKscHH+T0TYAJMwB4CotTjl3kL Z+Wstqd8+0tNuhGSgby4WPg6BbpRDKWXjWP8ygSYABOwicB6v/SdksxiRNyTVpCo8eR+o5PMpjDo /7PpIeRimQATSHICAuInvyZOLi7s+VOSo0ges+/wHS3FkLFl9wiJv4W/js8yASbABEwkIMU3fnhG FBd23WRirq7NKqlHSLt7TcglM3JuFVL8lR2y7qbC75gAE1BIQOKj+pSGXBaj3YyT1qhhN4I93w0Z WzpRSDwBwLPnGf7EBJgAEzCNwLuZtThn3rycGtNyTICMWJBCdOKQgo2jBASFBc4KcZoPMQEmwARi JiAgnivv0O1K2hcZcyYJeiMLUgsdS5FnRSAwTwBdWriEDzMBJsAEoiQg/7l4Zs4dgJBR3pgUl7Mg henm3Pz1fb3CO19C9g1zGZ9iAkyACbRGwC+Aq4pm5jzV2oXJfJ4FqZXezx1f2tnjxxwAJ7ZyKZ9m AkyACYQiUCGAsUUzcxaEOsnHdhNgK7vdLEK+K34tZ2tqZfVQ3kAbEg8fZAJMIDyBtQGPOIHFKDwk 4yyPkAwSEbzmFZTeCuBfyRS2IwIsfAkTYAIhCYjlnkZx1sI53X4LeZoP7kWABWkvJOEPDMkvPVsI vAigTfgr+SwTYAJJS0DI51N31vxp/vx+9UnLIIaGsyDFAG342E2DAlLOYWOHGODxLUwgsQn4hJA3 Fs3o/p/Ebqaa1rEgxcj1xAm/dEzzpbwK4NQYs+DbmAATSCACehwjTRQUTc8uTqBmWdoUNmqIEfey V/cv7ySzzwBwL7sbihEi38YEEoWAxOfw4WgWo/g6lEdI8fHT7x6aX3a6FPIFAPuYkB1nwQSYgIsI CCEeTdlZdROvF8XfaSxI8TPUcxiev6mXH4GZEDjKpCw5GybABJxNoAoCf1o8I+cVZ1fTPbXjKTuT +mphYbe1qVXVJ9KvJZ7CMwkqZ8MEnEtgpcePo1mMzO0gHiGZy1PPbejYjWdKKZ7hKTwFcDlLJmAz ASnk44GMhhuLn+9VZ3NVEq54FiRFXZo7ZkMPzau9LIBTFBXB2TIBJmAtge1SYuKSwhxyJcZJAQGe slMAlbIsnt1jwz4yOw8Q/wAku5lXxJmzZQIWEVgqJA5nMVJLm0dIavnquQ8bV3pUIACywhtgQXFc BBNgAuYRqBPAbScOyH548mQRMC9bzikUARakUFQUHDsuf31Gpua5FxLXsi88BYA5SyZgNgGJLwFx /uLC7FVmZ835hSbAghSai7KjeQVlgwXw5P+3d26xVVRRGP7/PbW2oqYgSIGC8a6QoPLgJYKppYL1 Fi+p0AcUrw/6oDHxHhU1XuOLiYmJqVGjRvAIGjERQ9GCGNEgmGgUlYhSORRRqQhKy5lZZho1EUs9 9zMz5386ObPXWnutb03yZ86Zvbe2HSoZYgUWgUIJDBj48K8NjQ/rVNdCUebmL0HKjVdRrAeflliz ALBbANQUJaiCiIAIFEzAgA/DPS8CAAAGbElEQVRovF5PRQWjzCuABCkvbMVxap275RQL2AlgWnEi KooIiECeBHYCvGvG5Man9V9RngSL4CZBKgLEQkI0N1uNO7z3ZsIeAFBfSCz5ioAI5EOAS/2Mf0P4 Zmw+3vIpHgEJUvFYFhSp9bJtRwUueIa0mQUFkrMIiEC2BLbB7KYVqQmLsnWQXWkJSJBKyzfH6MaW Oen5ND4OYHSOzjIXARHIjoABfK7GMre+k5r4S3YusioHAQlSOSjnOMfs9p5RGboHAV6vlx5yhCdz ERiewDrS3di1qHHN8GYarQQBCVIlqGc55+BLDz6fBDEjSxeZiYAIDE3gJxrunj5lXKdeWhgaUBSu SpCi0IVhczC2XJ6eS/BRAJOGNdWgCIjAvgQGQDzl9/c/2P3GkX37Dup7tAhIkKLVj/1mE65dGgHv JiNuB9CwX0MNiIAI/E3gdd/827pTEzf+fUGf0SYgQYp2f/6TXXNHerTn2/0ArgN4wH8MdEEEqpwA zT6C5+7QceLxuxEkSPHr2WDGze09x3j0FgDoAKBd22PaR6VdVAJfkLina9G41wFaUSMrWFkISJDK grl0k8zs6JmKjHsI5AWlm0WRRSDCBIjvYbxvlDW+lErRj3CmSu1/CEiQ/gdQXIZb5/SebkFwL4i2 uOSsPEWgQAKbaXx0JH55NpWaMlBgLLlHgIAEKQJNKGYKLe1bTyXtXgDnFzOuYolAhAh8R8MjI7Hj eQlRhLpShFQkSEWAGMUQ4aGAvo87SVys/5ii2CHllAeBrwh7fEfD+Bd1LEQe9GLgIkGKQZMKSfHs 9i3HO8dbYZgHoLaQWPIVgQoR+JjEY9NPHPeGFrVWqANlmlaCVCbQlZ7mnLmbxwfm3QzjdVrHVOlu aP4sCAQElhn4xIpXx72Xhb1MEkBAgpSAJuZSwqx5vSP8Absy3OUYwHG5+MpWBMpAYDdgL4D25IpF TV+XYT5NESECEqQINaOcqSxYYG7lF73nerAbDZgNwCvn/JpLBP5FgPjGjM8EA3s6tcXPv8hU1RcJ UlW1e+hiW9vTkwLyKsKu1n55QzPS1ZIQ6AfwGh07uxY2rtRi1pIwjlVQCVKs2lXaZMOnplVf9s5G YNeSdqG2Jiot7yqO/hkMz9bAf1HnEVXxXTBE6RKkIaDoEjCzY9tY8zPzSV4Dw7FiIgIFEtgF8FWY 37ki1fRhgbHknlACEqSENrZ4ZYXHX2ydQeIKGi41YGTxYitSwgkEAFbC7OXa2rrU2y8ftjPh9aq8 AglIkAoEWE3ubW3fHDhwyIg2g3UQvBBAfTXVr1qzJGBYC+AV52UWLl84KZ2ll8xEABIk3QR5ETjz ou2H1B04cB7Jyww4D8CIvALJKQkEDLCPaG4JAre4a/HYb5NQlGooPwEJUvmZJ27G8PDAg+iFr45f YkAbgTGJK1IF7UsgA9gqI5cGe4PXupc0/bCvgb6LQK4EJEi5EpP9sATa28372UufxoAXwHg+aFOH ddBgnAj8BGAZzN4i6pd1pUb9GqfklWv0CUiQot+jWGfYfOkPTTU1nGVw5xhspp6eYtXO8EiHNQSW +84tH+2PXavzhmLVv9glK0GKXcvim3C4zmn1hvRJ8F2rITgL5HTtqxepfvowrIfDKgZ8NwNvZXfq 8F2RylDJJJqABCnR7Y12ceHPezu89FTzeRaIGQBOBzAh2lknKrvdMKwDsdrI9/v/qFn9wZtjfktU hSomVgQkSLFqV/KTbWnvmeCcdxrMzjDwVAAnAzg0+ZWXvMKMARsIfGLAGs+CNXu3T/i8u5uZks+s CUQgSwISpCxByaxSBIwtc348CgimATaNNihQk7Xn3rD92EmzL438FMT6gO7Tg3cHny1dOv73Yb00 KAIVJiBBqnADNH1+BMJ1ULUH+ZO9wJ9ixAk0d7yZHQ3iaAB1+UWNlZcBCF+13mjARho2mLPPXcAN Xanxm2NViZIVgb8ISJB0KySMgHHW3HSTH7hjBp+iDEcYrYmGpsHvxMSY/AQY7oS9zYAewLYQDD83 G7DJkRsz9f2bup8/ck/CmqdyqpyABKnKb4BqLL95/qa62j21Y3xzjQDHGvwxNDfKYA2ObDCzBpg1 GFy9ozWY8QA4Oxhm9QT/efraz75+vxMIxQTh9gUg+mDYS2CXGcKxPwJaH8k+GnfArC8g+hyw3Rx+ 9PZie+DqtmqNTzXemar5T7boKrYfCqI6AAAAAElFTkSuQmCC"/></symbol><symbol viewBox="0 0 24 24" id="webpack" xmlns="http://www.w3.org/2000/svg"><path d="M19.376 15.988l-7.709 4.45-7.708-4.45V7.087l7.708-4.45 7.709 4.45z" fill="#fff" fill-opacity=".785" stroke-width="0"/><path d="M12.286 1.98c-.21 0-.41.059-.57.179l-7.9 4.44c-.32.17-.53.5-.53.88v9c0 .38.21.711.53.881l7.9 4.44c.16.12.36.18.57.18.21 0 .41-.06.57-.18l7.9-4.44c.32-.17.53-.5.53-.88v-9c0-.38-.21-.712-.53-.882l-7.9-4.44a.945.945 0 0 0-.57-.179zm0 2.15l7 3.939v2.104h-.016v5.177h.016v.54l-7 3.939-7-3.94V8.07l7-3.94zm0 2.08l-4.9 2.83 4.9 2.83 4.9-2.83-4.9-2.83zm-5 5.08v3.58l4 2.308v-3.58l-4-2.308zm10 0l-4 2.308v3.58l4-2.308v-3.58z" fill="#8ed6fb"/><path d="M12.286 6.21l-4.9 2.83 4.9 2.83 4.9-2.83-4.9-2.83zm-5 5.08v3.58l4 2.308v-3.58l-4-2.308zm10 0l-4 2.308v3.58l4-2.308v-3.58z" fill="#1c78c0"/></symbol><symbol viewBox="0 0 24 24" id="wolframlanguage" xmlns="http://www.w3.org/2000/svg"><title>wolframLanguage</title><g transform="scale(.12121)" fill="none" fill-rule="evenodd"><circle cx="99.197" cy="98.946" r="83.28" fill="#212121" stroke-width=".841"/><path d="M182.529 98.828a83.406 83.406 0 0 1-39.14 70.721.064.064 0 0 1-.038.019l-28.62-35.665 23.71 2.612s11.385 1.177 13.978 0c2.373-.938 15.175-18.963 15.175-18.963s-36.75-23.23-49.312-36.032c1.434-21.575-1.656-50.269-1.656-50.03-9.251 9.234-10.429 10.669-19.68 19.203-4.028-13.04-5.923-17.547-9.95-30.588-12.104 9.95-21.337 26.799-27.977 46.48a78.68 78.68 0 0 0-4.23 5.094 109.774 109.774 0 0 0-2.667 3.66 114.558 114.558 0 0 0-5.132 8.002 172.555 172.555 0 0 0-3.403 6.051c-7.706 14.475-14.034 31.066-19.515 46.001a.858.858 0 0 1-.092-.184c-14.988-30.912-9.502-67.85 13.822-93.072 23.325-25.223 59.723-33.575 91.71-21.045 31.988 12.53 53.029 43.382 53.017 77.736z" fill="#e53935"/><path d="M101.452 69.178s-1.416-8.295-2.373-11.367c6.401-6.18 7.357-7.118 13.52-13.04.477 11.845.238 18.006-.479 32.481-3.55-3.568-10.668-8.074-10.668-8.074zm-27.737 40.778s-6.64-4.029-11.624-4.728c1.435-3.329 5.223-7.596 6.18-8.773-1.913.699-15.653 6.86-17.087 12.084a74.804 74.804 0 0 1 11.385 3.79 35.993 35.993 0 0 0-8.774 20.158s21.815-3.33 38.185-1.196c.283.168.609.251.938.24l8.534.239 27.111 45.136.221.35c-.037.018-.055.037-.073.037-51.133 18.485-88.085-15.543-95.976-27.443.034-.102.058-.206.074-.313 7.1-30.017 15.855-65.939 30-76.552 7.356-12.82 9.49-31.783 22.751-41.734 3.33 9.951 8.553 30.588 12.103 40.539 15.653 15.652 39.361 35.094 55.234 43.15 1.656.956 3.79 7.596 3.79 7.596l-6.401 8.056-68.276-6.879a54.462 54.462 0 0 0-4.58-.183 86.848 86.848 0 0 0-14.144 1.36c3.311-8.295 10.43-14.935 10.43-14.935zm22.054-8.774c3.789-.46 7.817.956 12.323 3.568 4.267-1.195 4.745-1.434 9.013-2.612-5.463-4.028-11.386-8.295-19.442-7.118a47.249 47.249 0 0 0-1.894 6.162z" fill="#fff" stroke-width=".936"/></g></symbol><symbol viewBox="0 0 24 24" id="word" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m7 1.5V9h5.5L13 3.5M7 13l1.5 7h2l1.5-3 1.5 3h2l1.5-7h1v-2h-4v2h1l-.9 4.2L13 15h-2l-1.1 2.2L9 13h1v-2H6v2h1z" fill="#01579b"/></symbol><symbol viewBox="0 0 24 24" id="xaml" xmlns="http://www.w3.org/2000/svg"><path d="M18.93 12l-3.47 6H8.54l-3.47-6 3.47-6h6.92l3.47 6m4.84 0l-4.04 7L18 18l3.46-6L18 6l1.73-1 4.04 7M.23 12l4.04-7L6 6l-3.46 6L6 18l-1.73 1-4.04-7z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 24 24" id="xml" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m.12 13.5l3.74 3.74 1.42-1.41-2.33-2.33 2.33-2.33-1.42-1.41-3.74 3.74m11.16 0l-3.74-3.74-1.42 1.41 2.33 2.33-2.33 2.33 1.42 1.41 3.74-3.74z" fill="#8bc34a"/></symbol><symbol viewBox="0 0 24 24" id="yaml" xmlns="http://www.w3.org/2000/svg"><path d="M5 3h2v2H5v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5h2v2H5c-1.07-.27-2-.9-2-2v-4a2 2 0 0 0-2-2H0v-2h1a2 2 0 0 0 2-2V5a2 2 0 0 1 2-2m14 0a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1v2h-1a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-2v-2h2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5h-2V3h2m-7 12a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m-4 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1z" fill="#f44336"/></symbol><symbol viewBox="0 0 24 24" id="yang" xmlns="http://www.w3.org/2000/svg"><path d="M12 2a10 10 0 0 1 10 10 10 10 0 0 1-10 10A10 10 0 0 1 2 12 10 10 0 0 1 12 2m0 2a8 8 0 0 0-8 8 8 8 0 0 0 8 8 4 4 0 0 1-4-4 4 4 0 0 1 4-4 4 4 0 0 0 4-4 4 4 0 0 0-4-4m0 2.5A1.5 1.5 0 0 1 13.5 8 1.5 1.5 0 0 1 12 9.5 1.5 1.5 0 0 1 10.5 8 1.5 1.5 0 0 1 12 6.5m0 8a1.5 1.5 0 0 0-1.5 1.5 1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5 1.5 1.5 0 0 0-1.5-1.5z" fill="#42a5f5"/></symbol><symbol viewBox="0 0 289.99999 290.00001" id="yarn" xmlns="http://www.w3.org/2000/svg"><path d="M250.733 218.418c-12.39 2.943-18.661 5.653-33.993 15.641-24.004 15.487-50.176 22.688-50.176 22.688s-2.168 3.252-8.44 4.723c-10.84 2.633-51.647 4.878-55.364 4.956-9.988.077-16.105-2.555-17.809-6.66-5.188-12.388 7.434-17.809 7.434-17.809s-2.788-1.703-4.414-3.252c-1.471-1.47-3.02-4.413-3.484-3.33-1.936 4.724-2.943 16.261-8.13 21.45-7.125 7.2-20.598 4.8-28.573.619-8.75-4.646.62-15.564.62-15.564s-4.724 2.788-8.518-2.942c-3.407-5.266-6.582-14.248-5.73-25.32 1.084-12.777 15.176-25.011 15.176-25.011s-2.477-18.661 5.653-37.787c7.356-17.422 27.179-31.437 27.179-31.437s-16.648-18.352-10.454-35c4.027-10.84 5.653-10.763 6.97-11.227 4.645-1.781 9.136-3.717 12.466-7.356 16.648-17.964 37.864-14.557 37.864-14.557s9.911-30.431 19.203-24.469c2.865 1.859 13.163 24.778 13.163 24.778s10.996-6.426 12.235-4.026c6.659 12.931 7.433 37.632 4.49 52.654-4.955 24.778-17.344 38.096-22.3 46.459-1.161 1.936 13.319 8.053 22.456 33.373 8.44 23.152.929 42.587 2.245 44.756.232.387.31.542.31.542s9.679.774 29.114-11.228c10.376-6.427 22.688-13.628 36.703-13.783 13.55-.232 14.247 15.719 4.104 18.12z" fill="#2c8ebb" stroke-width=".774"/></symbol><symbol viewBox="0 0 24 24" id="zip" xmlns="http://www.w3.org/2000/svg"><path d="M14 17h-2v-2h-2v-2h2v2h2m0-6h-2v2h2v2h-2v-2h-2V9h2V7h-2V5h2v2h2m5-4H5c-1.11 0-2 .89-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="#afb42b"/></symbol></svg> 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 b4803be4d52..f89533aeb1d 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,8 +1,7 @@ import PayloadPreviewer from '~/pages/admin/application_settings/payload_previewer'; export default () => { - new PayloadPreviewer( - document.querySelector('.js-usage-ping-payload-trigger'), - document.querySelector('.js-usage-ping-payload'), - ).init(); + 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 new file mode 100644 index 00000000000..2ea55d44420 --- /dev/null +++ b/app/assets/javascripts/admin/cohorts/components/usage_ping_disabled.vue @@ -0,0 +1,48 @@ +<script> +import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui'; + +export default { + components: { + GlEmptyState, + GlSprintf, + GlLink, + }, + inject: { + svgPath: { + type: String, + }, + docsLink: { + type: String, + }, + primaryButtonPath: { + type: String, + }, + }, +}; +</script> +<template> + <gl-empty-state + class="js-empty-state" + :title="__('Activate user activity analysis')" + :svg-path="svgPath" + :primary-button-text="__('Turn on usage ping')" + :primary-button-link="primaryButtonPath" + > + <template #description> + <gl-sprintf + :message=" + __( + 'Turn on %{strongStart}usage ping%{strongEnd} to activate analysis of user activity, known as %{docLinkStart}Cohorts%{docLinkEnd}.', + ) + " + > + <template #docLink="{content}"> + <gl-link :href="docsLink" target="_blank">{{ content }}</gl-link> + </template> + <template #strong="{ content }" + ><strong>{{ content }}</strong></template + > + </gl-sprintf> + </template> + </gl-empty-state> +</template> 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 new file mode 100644 index 00000000000..5429ec403d3 --- /dev/null +++ b/app/assets/javascripts/admin/dev_ops_report/components/usage_ping_disabled.vue @@ -0,0 +1,53 @@ +<script> +import { GlEmptyState, GlSprintf, GlLink, GlButton } from '@gitlab/ui'; + +export default { + components: { + GlEmptyState, + GlSprintf, + GlLink, + GlButton, + }, + inject: { + isAdmin: { + type: Boolean, + }, + svgPath: { + type: String, + }, + docsLink: { + type: String, + }, + primaryButtonPath: { + type: String, + }, + }, +}; +</script> +<template> + <gl-empty-state class="js-empty-state" :title="__('Usage ping is off')" :svg-path="svgPath"> + <template #description> + <gl-sprintf + v-if="!isAdmin" + :message=" + __( + 'To view instance-level analytics, ask an admin to turn on %{docLinkStart}usage ping%{docLinkEnd}.', + ) + " + > + <template #docLink="{content}"> + <gl-link :href="docsLink" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + <template v-else + ><p> + {{ __('Turn on usage ping to review instance-level analytics.') }} + </p> + + <gl-button category="primary" variant="success" :href="primaryButtonPath"> + {{ __('Turn on usage ping') }}</gl-button + > + </template> + </template> + </gl-empty-state> +</template> diff --git a/app/assets/javascripts/admin/statistics_panel/store/getters.js b/app/assets/javascripts/admin/statistics_panel/store/getters.js index 2aa34b8f38e..1c1868b5bca 100644 --- a/app/assets/javascripts/admin/statistics_panel/store/getters.js +++ b/app/assets/javascripts/admin/statistics_panel/store/getters.js @@ -3,7 +3,6 @@ * and returns an array of the following form: * [{ key: "forks", label: "Forks", value: 50 }] */ -// eslint-disable-next-line import/prefer-default-export export const getStatistics = state => labels => Object.keys(labels).map(key => { const result = { diff --git a/app/assets/javascripts/ajax_loading_spinner.js b/app/assets/javascripts/ajax_loading_spinner.js deleted file mode 100644 index 54e86f329e4..00000000000 --- a/app/assets/javascripts/ajax_loading_spinner.js +++ /dev/null @@ -1,34 +0,0 @@ -import $ from 'jquery'; - -export default class AjaxLoadingSpinner { - static init() { - const $elements = $('.js-ajax-loading-spinner'); - - $elements.on('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend); - $elements.on('ajax:complete', AjaxLoadingSpinner.ajaxComplete); - } - - static ajaxBeforeSend(e) { - e.target.setAttribute('disabled', ''); - const iconElement = e.target.querySelector('i'); - // get first fa- icon - const originalIcon = iconElement.className.match(/(fa-)([^\s]+)/g)[0]; - iconElement.dataset.icon = originalIcon; - AjaxLoadingSpinner.toggleLoadingIcon(iconElement); - $(e.target).off('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend); - } - - static ajaxComplete(e) { - e.target.removeAttribute('disabled'); - const iconElement = e.target.querySelector('i'); - AjaxLoadingSpinner.toggleLoadingIcon(iconElement); - $(e.target).off('ajax:complete', AjaxLoadingSpinner.ajaxComplete); - } - - static toggleLoadingIcon(iconElement) { - const { classList } = iconElement; - classList.toggle(iconElement.dataset.icon); - classList.toggle('fa-spinner'); - classList.toggle('fa-spin'); - } -} diff --git a/app/assets/javascripts/alert_handler.js b/app/assets/javascripts/alert_handler.js new file mode 100644 index 00000000000..8fffb61d1dd --- /dev/null +++ b/app/assets/javascripts/alert_handler.js @@ -0,0 +1,13 @@ +// This allows us to dismiss alerts that we've migrated from bootstrap +// Note: This ONLY works on alerts that are created on page load +// You can follow this effort in the following epic +// https://gitlab.com/groups/gitlab-org/-/epics/4070 + +export default function initAlertHandler() { + const ALERT_SELECTOR = '.gl-alert'; + const CLOSE_SELECTOR = '.gl-alert-dismiss'; + + const dismissAlert = ({ target }) => target.closest(ALERT_SELECTOR).remove(); + const closeButtons = document.querySelectorAll(`${ALERT_SELECTOR} ${CLOSE_SELECTOR}`); + closeButtons.forEach(alert => alert.addEventListener('click', dismissAlert)); +} diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue index 5d260fcc200..c6605452616 100644 --- a/app/assets/javascripts/alert_management/components/alert_details.vue +++ b/app/assets/javascripts/alert_management/components/alert_details.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import * as Sentry from '@sentry/browser'; import { GlAlert, @@ -9,7 +10,6 @@ import { GlTabs, GlTab, GlButton, - GlTable, } from '@gitlab/ui'; import { s__ } from '~/locale'; import alertQuery from '../graphql/queries/details.query.graphql'; @@ -27,6 +27,7 @@ import { toggleContainerClasses } from '~/lib/utils/dom_utils'; import SystemNote from './system_notes/system_note.vue'; import AlertSidebar from './alert_sidebar.vue'; import AlertMetrics from './alert_metrics.vue'; +import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; const containerEl = document.querySelector('.page-with-contextual-sidebar'); @@ -42,18 +43,19 @@ export default { tabsConfig: [ { id: 'overview', - title: s__('AlertManagement|Overview'), - }, - { - id: 'fullDetails', title: s__('AlertManagement|Alert details'), }, { id: 'metrics', title: s__('AlertManagement|Metrics'), }, + { + id: 'activity', + title: s__('AlertManagement|Activity feed'), + }, ], components: { + AlertDetailsTable, GlBadge, GlAlert, GlIcon, @@ -62,7 +64,6 @@ export default { GlTab, GlTabs, GlButton, - GlTable, TimeAgoTooltip, AlertSidebar, SystemNote, @@ -330,32 +331,17 @@ export default { </div> <div class="gl-pl-2" data-testid="runbook">{{ alert.runbook }}</div> </div> - <template> - <div v-if="alert.notes.nodes" class="issuable-discussion py-5"> - <ul class="notes main-notes-list timeline"> - <system-note v-for="note in alert.notes.nodes" :key="note.id" :note="note" /> - </ul> - </div> - </template> + <alert-details-table :alert="alert" :loading="loading" /> </gl-tab> <gl-tab :data-testid="$options.tabsConfig[1].id" :title="$options.tabsConfig[1].title"> - <gl-table - class="alert-management-details-table" - :items="[{ key: 'Value', ...alert }]" - :show-empty="true" - :busy="loading" - stacked - > - <template #empty> - {{ s__('AlertManagement|No alert data to display.') }} - </template> - <template #table-busy> - <gl-loading-icon size="lg" color="dark" class="mt-3" /> - </template> - </gl-table> + <alert-metrics :dashboard-url="alert.metricsDashboardUrl" /> </gl-tab> <gl-tab :data-testid="$options.tabsConfig[2].id" :title="$options.tabsConfig[2].title"> - <alert-metrics :dashboard-url="alert.metricsDashboardUrl" /> + <div v-if="alert.notes.nodes.length > 0" class="issuable-discussion"> + <ul class="notes main-notes-list timeline"> + <system-note v-for="note in alert.notes.nodes" :key="note.id" :note="note" /> + </ul> + </div> </gl-tab> </gl-tabs> <alert-sidebar 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 92fd85c6217..0fd00fe90eb 100644 --- a/app/assets/javascripts/alert_management/components/alert_management_table.vue +++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue @@ -1,8 +1,12 @@ <script> +/* eslint-disable vue/no-v-html */ import { GlLoadingIcon, GlTable, GlAlert, + GlAvatarsInline, + GlAvatarLink, + GlAvatar, GlIcon, GlLink, GlTabs, @@ -11,6 +15,7 @@ import { GlPagination, GlSearchBoxByType, GlSprintf, + GlTooltipDirective, } from '@gitlab/ui'; import { debounce, trim } from 'lodash'; import { __, s__ } from '~/locale'; @@ -35,6 +40,7 @@ const tdClass = const thClass = 'gl-hover-bg-blue-50'; const bodyTrClass = 'gl-border-1 gl-border-t-solid gl-border-gray-100 gl-hover-bg-blue-50 gl-hover-cursor-pointer gl-hover-border-b-solid gl-hover-border-blue-200'; +const TH_TEST_ID = { 'data-testid': 'alert-management-severity-sort' }; const initialPaginationState = { currentPage: 1, @@ -55,12 +61,14 @@ export default { "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear.", ), searchPlaceholder: __('Search or filter results...'), + unassigned: __('Unassigned'), }, fields: [ { key: 'severity', label: s__('AlertManagement|Severity'), thClass: `${thClass} gl-w-eighth`, + thAttr: TH_TEST_ID, tdClass: `${tdClass} rounded-top text-capitalize sortable-cell`, sortable: true, }, @@ -72,7 +80,7 @@ export default { sortable: true, }, { - key: 'title', + key: 'alertLabel', label: s__('AlertManagement|Alert'), thClass: `gl-pointer-events-none`, tdClass, @@ -110,6 +118,9 @@ export default { GlLoadingIcon, GlTable, GlAlert, + GlAvatarsInline, + GlAvatarLink, + GlAvatar, TimeAgo, GlIcon, GlLink, @@ -121,6 +132,9 @@ export default { GlSprintf, AlertStatus, }, + directives: { + GlTooltip: GlTooltipDirective, + }, props: { projectPath: { type: String, @@ -264,11 +278,8 @@ export default { const { category, action, label } = trackAlertStatusUpdateOptions; Tracking.event(category, action, { label, property: status }); }, - getAssignees(assignees) { - // TODO: Update to show list of assignee(s) after https://gitlab.com/gitlab-org/gitlab/-/issues/218405 - return assignees.nodes?.length > 0 - ? assignees.nodes[0]?.username - : s__('AlertManagement|Unassigned'); + hasAssignees(assignees) { + return Boolean(assignees.nodes?.length); }, getIssueLink(item) { return joinPaths('/', this.projectPath, '-', 'issues', item.issueIid); @@ -397,8 +408,14 @@ export default { {{ item.eventCount }} </template> - <template #cell(title)="{ item }"> - <div class="gl-max-w-full text-truncate" :title="item.title">{{ item.title }}</div> + <template #cell(alertLabel)="{ item }"> + <div + class="gl-max-w-full text-truncate" + :title="`${item.iid} - ${item.title}`" + data-testid="idField" + > + #{{ item.iid }} {{ item.title }} + </div> </template> <template #cell(issue)="{ item }"> @@ -409,8 +426,32 @@ export default { </template> <template #cell(assignees)="{ item }"> - <div class="gl-max-w-full text-truncate" data-testid="assigneesField"> - {{ getAssignees(item.assignees) }} + <div data-testid="assigneesField"> + <template v-if="hasAssignees(item.assignees)"> + <gl-avatars-inline + :avatars="item.assignees.nodes" + :collapsed="true" + :max-visible="4" + :avatar-size="24" + badge-tooltip-prop="name" + :badge-tooltip-max-chars="100" + > + <template #avatar="{ avatar }"> + <gl-avatar-link + :key="avatar.username" + v-gl-tooltip + target="_blank" + :href="avatar.webUrl" + :title="avatar.name" + > + <gl-avatar :src="avatar.avatarUrl" :label="avatar.name" :size="24" /> + </gl-avatar-link> + </template> + </gl-avatars-inline> + </template> + <template v-else> + {{ $options.i18n.unassigned }} + </template> </div> </template> diff --git a/app/assets/javascripts/alert_management/components/alert_status.vue b/app/assets/javascripts/alert_management/components/alert_status.vue index 8531ca1374e..ff71b348cc9 100644 --- a/app/assets/javascripts/alert_management/components/alert_status.vue +++ b/app/assets/javascripts/alert_management/components/alert_status.vue @@ -101,12 +101,12 @@ export default { @keydown.esc.native="$emit('hide-dropdown')" @hide="$emit('hide-dropdown')" > - <div v-if="isSidebar" class="dropdown-title text-center"> - <span class="alert-title">{{ s__('AlertManagement|Assign status') }}</span> + <div v-if="isSidebar" class="dropdown-title gl-display-flex"> + <span class="alert-title gl-ml-auto">{{ s__('AlertManagement|Assign status') }}</span> <gl-button :aria-label="__('Close')" variant="link" - class="dropdown-title-button dropdown-menu-close" + class="dropdown-title-button dropdown-menu-close gl-ml-auto gl-text-black-normal!" icon="close" @click="$emit('hide-dropdown')" /> 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 4af5c83b30c..0f354e85e96 100644 --- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue +++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue @@ -12,7 +12,7 @@ import { } from '@gitlab/ui'; import { debounce } from 'lodash'; import axios from '~/lib/utils/axios_utils'; -import { s__, __ } from '~/locale'; +import { s__ } from '~/locale'; import alertSetAssignees from '../../graphql/mutations/alert_set_assignees.mutation.graphql'; import SidebarAssignee from './sidebar_assignee.vue'; @@ -82,8 +82,11 @@ export default { userName() { return this.alert?.assignees?.nodes[0]?.username; }, - assignedUser() { - return this.userName || __('None'); + userFullName() { + return this.alert?.assignees?.nodes[0]?.name; + }, + userImg() { + return this.alert?.assignees?.nodes[0]?.avatarUrl; }, sortedUsers() { return this.users @@ -184,15 +187,15 @@ export default { </script> <template> - <div class="block alert-status"> - <div ref="status" class="sidebar-collapsed-icon" @click="$emit('toggle-sidebar')"> + <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" /> </div> - <gl-tooltip :target="() => $refs.status" boundary="viewport" placement="left"> + <gl-tooltip :target="() => $refs.assignees" boundary="viewport" placement="left"> <gl-sprintf :message="$options.i18n.ASSIGNEES_BLOCK"> <template #assignees> - {{ assignedUser }} + {{ userName }} </template> </gl-sprintf> </gl-tooltip> @@ -215,19 +218,19 @@ export default { <div class="dropdown dropdown-menu-selectable" :class="dropdownClass"> <gl-deprecated-dropdown ref="dropdown" - :text="assignedUser" + :text="userName" class="w-100" toggle-class="dropdown-menu-toggle" variant="outline-default" @keydown.esc.native="hideDropdown" @hide="hideDropdown" > - <div class="dropdown-title"> - <span class="alert-title">{{ __('Assign To') }}</span> + <div class="dropdown-title gl-display-flex"> + <span class="alert-title gl-ml-auto">{{ __('Assign To') }}</span> <gl-button :aria-label="__('Close')" variant="link" - class="dropdown-title-button dropdown-menu-close" + class="dropdown-title-button dropdown-menu-close gl-ml-auto gl-text-black-normal!" icon="close" @click="hideDropdown" /> @@ -272,14 +275,28 @@ export default { </div> <gl-loading-icon v-if="isUpdating" :inline="true" /> - <p v-else-if="!isDropdownShowing" class="value gl-m-0" :class="{ 'no-value': !userName }"> - <span v-if="userName" class="gl-text-gray-500" data-testid="assigned-users">{{ - assignedUser - }}</span> - <span v-else class="gl-display-flex gl-align-items-center"> + <div v-else-if="!isDropdownShowing" class="value gl-m-0" :class="{ 'no-value': !userName }"> + <div v-if="userName" class="gl-display-inline-flex gl-mt-2" data-testid="assigned-users"> + <span class="gl-relative mr-2"> + <img + :alt="userName" + :src="userImg" + :width="32" + class="avatar avatar-inline gl-m-0 s32" + data-qa-selector="avatar_image" + /> + </span> + <span class="gl-display-flex gl-flex-direction-column gl-overflow-hidden"> + <strong class="dropdown-menu-user-full-name"> + {{ userFullName }} + </strong> + <span class="dropdown-menu-user-username">{{ userName }}</span> + </span> + </div> + <span v-else class="gl-display-flex gl-align-items-center gl-line-height-normal"> {{ __('None') }} - <gl-button - class="gl-pl-2" + class="gl-ml-2" href="#" variant="link" data-testid="unassigned-users" @@ -288,7 +305,7 @@ export default { {{ __('assign yourself') }} </gl-button> </span> - </p> + </div> </div> </div> </template> 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 5bd69a1f0ec..84d54466a10 100644 --- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue +++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue @@ -1,8 +1,9 @@ <script> +import produce from 'immer'; import { s__ } from '~/locale'; import Todo from '~/sidebar/components/todo_toggle/todo.vue'; -import createAlertTodo from '../../graphql/mutations/alert_todo_create.mutation.graphql'; -import todoMarkDone from '../../graphql/mutations/alert_todo_mark_done.mutation.graphql'; +import createAlertTodoMutation from '../../graphql/mutations/alert_todo_create.mutation.graphql'; +import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql'; import alertQuery from '../../graphql/queries/details.query.graphql'; export default { @@ -52,7 +53,7 @@ export default { }, methods: { updateToDoCount(add) { - const oldCount = parseInt(document.querySelector('.todos-count').innerText, 10); + const oldCount = parseInt(document.querySelector('.js-todos-count').innerText, 10); const count = add ? oldCount + 1 : oldCount - 1; const headerTodoEvent = new CustomEvent('todo:toggle', { detail: { @@ -66,7 +67,7 @@ export default { this.isUpdating = true; return this.$apollo .mutate({ - mutation: createAlertTodo, + mutation: createAlertTodoMutation, variables: { iid: this.alert.iid, projectPath: this.projectPath, @@ -89,7 +90,7 @@ export default { this.isUpdating = true; return this.$apollo .mutate({ - mutation: todoMarkDone, + mutation: todoMarkDoneMutation, variables: { id: this.firstToDoId, }, @@ -109,12 +110,15 @@ export default { }); }, updateCache(store) { - const data = store.readQuery({ + const sourceData = store.readQuery({ query: alertQuery, variables: this.getAlertQueryVariables, }); - data.project.alertManagementAlerts.nodes[0].todos.nodes.shift(); + const data = produce(sourceData, draftData => { + // eslint-disable-next-line no-param-reassign + draftData.project.alertManagementAlerts.nodes[0].todos.nodes = []; + }); store.writeQuery({ query: alertQuery, diff --git a/app/assets/javascripts/alert_management/components/system_notes/system_note.vue b/app/assets/javascripts/alert_management/components/system_notes/system_note.vue index 39717ab609f..0b206ce42f4 100644 --- a/app/assets/javascripts/alert_management/components/system_notes/system_note.vue +++ b/app/assets/javascripts/alert_management/components/system_notes/system_note.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import NoteHeader from '~/notes/components/note_header.vue'; import { spriteIcon } from '~/lib/utils/common_utils'; @@ -31,7 +32,7 @@ export default { </script> <template> - <li :id="noteAnchorId" class="timeline-entry note system-note note-wrapper"> + <li :id="noteAnchorId" class="timeline-entry note system-note note-wrapper gl-px-0!"> <div class="timeline-entry-inner"> <div class="timeline-icon" v-html="iconHtml"></div> <div class="timeline-content"> diff --git a/app/assets/javascripts/alert_management/constants.js b/app/assets/javascripts/alert_management/constants.js index b9670466c0f..73cb5ecdf98 100644 --- a/app/assets/javascripts/alert_management/constants.js +++ b/app/assets/javascripts/alert_management/constants.js @@ -64,4 +64,4 @@ export const trackAlertStatusUpdateOptions = { label: 'Status', }; -export const DEFAULT_PAGE_SIZE = 10; +export const DEFAULT_PAGE_SIZE = 20; diff --git a/app/assets/javascripts/alert_management/details.js b/app/assets/javascripts/alert_management/details.js index dccf990f0b4..c2020dfcbe3 100644 --- a/app/assets/javascripts/alert_management/details.js +++ b/app/assets/javascripts/alert_management/details.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import produce from 'immer'; import { defaultDataIdFromObject } from 'apollo-cache-inmemory'; import createDefaultClient from '~/lib/graphql'; import createRouter from './router'; @@ -16,8 +17,11 @@ export default selector => { const resolvers = { Mutation: { toggleSidebarStatus: (_, __, { cache }) => { - const data = cache.readQuery({ query: sidebarStatusQuery }); - data.sidebarStatus = !data.sidebarStatus; + const sourceData = cache.readQuery({ query: sidebarStatusQuery }); + const data = produce(sourceData, draftData => { + // eslint-disable-next-line no-param-reassign + draftData.sidebarStatus = !draftData.sidebarStatus; + }); cache.writeQuery({ query: sidebarStatusQuery, data }); }, }, @@ -34,6 +38,7 @@ export default selector => { return defaultDataIdFromObject(object); }, }, + assumeImmutableResults: true, }), }); diff --git a/app/assets/javascripts/alert_management/graphql/fragments/list_item.fragment.graphql b/app/assets/javascripts/alert_management/graphql/fragments/list_item.fragment.graphql index c37f29c74fc..62119177887 100644 --- a/app/assets/javascripts/alert_management/graphql/fragments/list_item.fragment.graphql +++ b/app/assets/javascripts/alert_management/graphql/fragments/list_item.fragment.graphql @@ -8,7 +8,10 @@ fragment AlertListItem on AlertManagementAlert { issueIid assignees { nodes { + name username + avatarUrl + webUrl } } } 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 40b4b6ae854..5008bfa5e1b 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 @@ -10,6 +10,9 @@ mutation alertSetAssignees($projectPath: ID!, $assigneeUsernames: [String!]!, $i assignees { nodes { username + name + avatarUrl + webUrl } } notes { diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 1d8fb1fc5a6..dbc7ff67d9d 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -20,6 +20,7 @@ const Api = { projectPath: '/api/:version/projects/:id', forkedProjectsPath: '/api/:version/projects/:id/forks', projectLabelsPath: '/:namespace_path/:project_path/-/labels', + projectFileSchemaPath: '/:namespace_path/:project_path/-/schema/:ref/:filename', projectUsersPath: '/api/:version/projects/:id/users', projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests', projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid', @@ -42,7 +43,6 @@ const Api = { userPostStatusPath: '/api/:version/user/status', commitPath: '/api/:version/projects/:id/repository/commits/:sha', commitsPath: '/api/:version/projects/:id/repository/commits', - applySuggestionPath: '/api/:version/suggestions/:id/apply', applySuggestionBatchPath: '/api/:version/suggestions/batch_apply', commitPipelinesPath: '/:project_id/commit/:sha/pipelines', @@ -309,10 +309,12 @@ const Api = { }); }, - projectMilestones(id) { + projectMilestones(id, params = {}) { const url = Api.buildUrl(Api.projectMilestonesPath).replace(':id', encodeURIComponent(id)); - return axios.get(url); + return axios.get(url, { + params, + }); }, mergeRequests(params = {}) { diff --git a/app/assets/javascripts/authentication/mount_2fa.js b/app/assets/javascripts/authentication/mount_2fa.js index 9917151ac81..dd5a42fa5fc 100644 --- a/app/assets/javascripts/authentication/mount_2fa.js +++ b/app/assets/javascripts/authentication/mount_2fa.js @@ -1,14 +1,23 @@ import $ from 'jquery'; import initU2F from './u2f'; +import initWebauthn from './webauthn'; import U2FRegister from './u2f/register'; +import WebAuthnRegister from './webauthn/register'; export const mount2faAuthentication = () => { - // Soon this will conditionally mount a webauthn app (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26692) - initU2F(); + if (gon.webauthn) { + initWebauthn(); + } else { + initU2F(); + } }; export const mount2faRegistration = () => { - // Soon this will conditionally mount a webauthn app (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26692) - const u2fRegister = new U2FRegister($('#js-register-u2f'), gon.u2f); - u2fRegister.start(); + if (gon.webauthn) { + const webauthnRegister = new WebAuthnRegister($('#js-register-token-2fa'), gon.webauthn); + webauthnRegister.start(); + } else { + const u2fRegister = new U2FRegister($('#js-register-token-2fa'), gon.u2f); + u2fRegister.start(); + } }; diff --git a/app/assets/javascripts/authentication/u2f/authenticate.js b/app/assets/javascripts/authentication/u2f/authenticate.js index 201cd5c2e61..f9b5ca3e5b4 100644 --- a/app/assets/javascripts/authentication/u2f/authenticate.js +++ b/app/assets/javascripts/authentication/u2f/authenticate.js @@ -40,7 +40,6 @@ export default class U2FAuthenticate { this.signRequests = u2fParams.sign_requests.map(request => omit(request, 'challenge')); this.templates = { - setup: '#js-authenticate-token-2fa-setup', inProgress: '#js-authenticate-token-2fa-in-progress', error: '#js-authenticate-token-2fa-error', authenticated: '#js-authenticate-token-2fa-authenticated', @@ -86,7 +85,7 @@ export default class U2FAuthenticate { renderError(error) { this.renderTemplate('error', { error_message: error.message(), - error_code: error.errorCode, + error_name: error.errorCode, }); return this.container.find('#js-token-2fa-try-again').on('click', this.renderInProgress); } diff --git a/app/assets/javascripts/authentication/u2f/register.js b/app/assets/javascripts/authentication/u2f/register.js index 52c0ce1fc04..9773a9185f8 100644 --- a/app/assets/javascripts/authentication/u2f/register.js +++ b/app/assets/javascripts/authentication/u2f/register.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import { template as lodashTemplate } from 'lodash'; +import { __ } from '~/locale'; import importU2FLibrary from './util'; import U2FError from './error'; @@ -24,11 +25,10 @@ export default class U2FRegister { this.signRequests = u2fParams.sign_requests; this.templates = { - notSupported: '#js-register-u2f-not-supported', - setup: '#js-register-u2f-setup', - inProgress: '#js-register-u2f-in-progress', - error: '#js-register-u2f-error', - registered: '#js-register-u2f-registered', + message: '#js-register-2fa-message', + setup: '#js-register-token-2fa-setup', + error: '#js-register-token-2fa-error', + registered: '#js-register-token-2fa-registered', }; } @@ -65,18 +65,22 @@ export default class U2FRegister { renderSetup() { this.renderTemplate('setup'); - return this.container.find('#js-setup-u2f-device').on('click', this.renderInProgress); + return this.container.find('#js-setup-token-2fa-device').on('click', this.renderInProgress); } renderInProgress() { - this.renderTemplate('inProgress'); + this.renderTemplate('message', { + message: __( + 'Trying to communicate with your device. Plug it in (if needed) and press the button on the device now.', + ), + }); return this.register(); } renderError(error) { this.renderTemplate('error', { error_message: error.message(), - error_code: error.errorCode, + error_name: error.errorCode, }); return this.container.find('#js-token-2fa-try-again').on('click', this.renderSetup); } @@ -89,6 +93,10 @@ export default class U2FRegister { } renderNotSupported() { - return this.renderTemplate('notSupported'); + return this.renderTemplate('message', { + message: __( + "Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).", + ), + }); } } diff --git a/app/assets/javascripts/authentication/webauthn/authenticate.js b/app/assets/javascripts/authentication/webauthn/authenticate.js new file mode 100644 index 00000000000..42c4c2b63bd --- /dev/null +++ b/app/assets/javascripts/authentication/webauthn/authenticate.js @@ -0,0 +1,69 @@ +import WebAuthnError from './error'; +import WebAuthnFlow from './flow'; +import { supported, convertGetParams, convertGetResponse } from './util'; + +// Authenticate WebAuthn devices for users to authenticate with. +// +// State Flow #1: setup -> in_progress -> authenticated -> POST to server +// State Flow #2: setup -> in_progress -> error -> setup +export default class WebAuthnAuthenticate { + constructor(container, form, webauthnParams, fallbackButton, fallbackUI) { + this.container = container; + this.webauthnParams = convertGetParams(JSON.parse(webauthnParams.options)); + this.renderInProgress = this.renderInProgress.bind(this); + + this.form = form; + this.fallbackButton = fallbackButton; + this.fallbackUI = fallbackUI; + if (this.fallbackButton) { + this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this)); + } + + this.flow = new WebAuthnFlow(container, { + inProgress: '#js-authenticate-token-2fa-in-progress', + error: '#js-authenticate-token-2fa-error', + authenticated: '#js-authenticate-token-2fa-authenticated', + }); + + this.container.on('click', '#js-token-2fa-try-again', this.renderInProgress); + } + + start() { + if (!supported()) { + this.switchToFallbackUI(); + } else { + this.renderInProgress(); + } + } + + authenticate() { + navigator.credentials + .get({ publicKey: this.webauthnParams }) + .then(resp => { + const convertedResponse = convertGetResponse(resp); + this.renderAuthenticated(JSON.stringify(convertedResponse)); + }) + .catch(err => { + this.flow.renderError(new WebAuthnError(err, 'authenticate')); + }); + } + + renderInProgress() { + this.flow.renderTemplate('inProgress'); + this.authenticate(); + } + + renderAuthenticated(deviceResponse) { + this.flow.renderTemplate('authenticated'); + const container = this.container[0]; + container.querySelector('#js-device-response').value = deviceResponse; + container.querySelector(this.form).submit(); + this.fallbackButton.classList.add('hidden'); + } + + switchToFallbackUI() { + this.fallbackButton.classList.add('hidden'); + this.container[0].classList.add('hidden'); + this.fallbackUI.classList.remove('hidden'); + } +} diff --git a/app/assets/javascripts/authentication/webauthn/error.js b/app/assets/javascripts/authentication/webauthn/error.js new file mode 100644 index 00000000000..a1a3f861c25 --- /dev/null +++ b/app/assets/javascripts/authentication/webauthn/error.js @@ -0,0 +1,28 @@ +import { __ } from '~/locale'; +import { isHTTPS, FLOW_AUTHENTICATE, FLOW_REGISTER } from './util'; + +export default class WebAuthnError { + constructor(error, flowType) { + this.error = error; + this.errorName = error.name || 'UnknownError'; + this.message = this.message.bind(this); + this.httpsDisabled = !isHTTPS(); + this.flowType = flowType; + } + + message() { + if (this.errorName === 'NotSupportedError') { + return __('Your device is not compatible with GitLab. Please try another device'); + } else if (this.errorName === 'InvalidStateError' && this.flowType === FLOW_AUTHENTICATE) { + return __('This device has not been registered with us.'); + } else if (this.errorName === 'InvalidStateError' && this.flowType === FLOW_REGISTER) { + return __('This device has already been registered with us.'); + } else if (this.errorName === 'SecurityError' && this.httpsDisabled) { + return __( + 'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.', + ); + } + + return __('There was a problem communicating with your device.'); + } +} diff --git a/app/assets/javascripts/authentication/webauthn/flow.js b/app/assets/javascripts/authentication/webauthn/flow.js new file mode 100644 index 00000000000..10a1debc876 --- /dev/null +++ b/app/assets/javascripts/authentication/webauthn/flow.js @@ -0,0 +1,24 @@ +import { template } from 'lodash'; + +/** + * Generic abstraction for WebAuthnFlows, especially for register / authenticate + */ +export default class WebAuthnFlow { + constructor(container, templates) { + this.container = container; + this.templates = templates; + } + + renderTemplate(name, params) { + const templateString = document.querySelector(this.templates[name]).innerHTML; + const compiledTemplate = template(templateString); + this.container.html(compiledTemplate(params)); + } + + renderError(error) { + this.renderTemplate('error', { + error_message: error.message(), + error_name: error.errorName, + }); + } +} diff --git a/app/assets/javascripts/authentication/webauthn/index.js b/app/assets/javascripts/authentication/webauthn/index.js new file mode 100644 index 00000000000..bbf694c7698 --- /dev/null +++ b/app/assets/javascripts/authentication/webauthn/index.js @@ -0,0 +1,13 @@ +import $ from 'jquery'; +import WebAuthnAuthenticate from './authenticate'; + +export default () => { + const webauthnAuthenticate = new WebAuthnAuthenticate( + $('#js-authenticate-token-2fa'), + '#js-login-token-2fa-form', + gon.webauthn, + document.querySelector('#js-login-2fa-device'), + document.querySelector('.js-2fa-form'), + ); + webauthnAuthenticate.start(); +}; diff --git a/app/assets/javascripts/authentication/webauthn/register.js b/app/assets/javascripts/authentication/webauthn/register.js new file mode 100644 index 00000000000..06e4ffd6f3e --- /dev/null +++ b/app/assets/javascripts/authentication/webauthn/register.js @@ -0,0 +1,78 @@ +import { __ } from '~/locale'; +import WebAuthnError from './error'; +import WebAuthnFlow from './flow'; +import { supported, isHTTPS, convertCreateParams, convertCreateResponse } from './util'; + +// Register WebAuthn devices for users to authenticate with. +// +// State Flow #1: setup -> in_progress -> registered -> POST to server +// State Flow #2: setup -> in_progress -> error -> setup +export default class WebAuthnRegister { + constructor(container, webauthnParams) { + this.container = container; + this.renderInProgress = this.renderInProgress.bind(this); + this.webauthnOptions = convertCreateParams(webauthnParams.options); + + this.flow = new WebAuthnFlow(container, { + message: '#js-register-2fa-message', + setup: '#js-register-token-2fa-setup', + error: '#js-register-token-2fa-error', + registered: '#js-register-token-2fa-registered', + }); + + this.container.on('click', '#js-token-2fa-try-again', this.renderInProgress); + } + + start() { + if (!supported()) { + // we show a special error message when the user visits the site + // using a non-ssl connection as this makes WebAuthn unavailable in + // any case, regardless of the used browser + this.renderNotSupported(!isHTTPS()); + } else { + this.renderSetup(); + } + } + + register() { + navigator.credentials + .create({ + publicKey: this.webauthnOptions, + }) + .then(cred => this.renderRegistered(JSON.stringify(convertCreateResponse(cred)))) + .catch(err => this.flow.renderError(new WebAuthnError(err, 'register'))); + } + + renderSetup() { + this.flow.renderTemplate('setup'); + this.container.find('#js-setup-token-2fa-device').on('click', this.renderInProgress); + } + + renderInProgress() { + this.flow.renderTemplate('message', { + message: __( + 'Trying to communicate with your device. Plug it in (if needed) and press the button on the device now.', + ), + }); + return this.register(); + } + + renderRegistered(deviceResponse) { + this.flow.renderTemplate('registered'); + // Prefer to do this instead of interpolating using Underscore templates + // because of JSON escaping issues. + this.container.find('#js-device-response').val(deviceResponse); + } + + renderNotSupported(noHttps) { + const message = noHttps + ? __( + 'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.', + ) + : __( + "Your browser doesn't support WebAuthn. Please use a supported browser, e.g. Chrome (67+) or Firefox (60+).", + ); + + this.flow.renderTemplate('message', { message }); + } +} diff --git a/app/assets/javascripts/authentication/webauthn/util.js b/app/assets/javascripts/authentication/webauthn/util.js new file mode 100644 index 00000000000..5f06c000afe --- /dev/null +++ b/app/assets/javascripts/authentication/webauthn/util.js @@ -0,0 +1,120 @@ +export function supported() { + return Boolean( + navigator.credentials && + navigator.credentials.create && + navigator.credentials.get && + window.PublicKeyCredential, + ); +} + +export function isHTTPS() { + return window.location.protocol.startsWith('https'); +} + +export const FLOW_AUTHENTICATE = 'authenticate'; +export const FLOW_REGISTER = 'register'; + +// adapted from https://stackoverflow.com/a/21797381/8204697 +function base64ToBuffer(base64) { + const binaryString = window.atob(base64); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i += 1) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes.buffer; +} + +// adapted from https://stackoverflow.com/a/9458996/8204697 +function bufferToBase64(buffer) { + if (typeof buffer === 'string') { + return buffer; + } + + let binary = ''; + const bytes = new Uint8Array(buffer); + const len = bytes.byteLength; + for (let i = 0; i < len; i += 1) { + binary += String.fromCharCode(bytes[i]); + } + return window.btoa(binary); +} + +/** + * Returns a copy of the given object with the id property converted to buffer + * + * @param {Object} param + */ +function convertIdToBuffer({ id, ...rest }) { + return { + ...rest, + id: base64ToBuffer(id), + }; +} + +/** + * Returns a copy of the given array with all `id`s of the items converted to buffer + * + * @param {Array} items + */ +function convertIdsToBuffer(items) { + return items.map(convertIdToBuffer); +} + +/** + * Returns an object with keys of the given props, and values from the given object converted to base64 + * + * @param {String} obj + * @param {Array} props + */ +function convertPropertiesToBase64(obj, props) { + return props.reduce( + (acc, property) => Object.assign(acc, { [property]: bufferToBase64(obj[property]) }), + {}, + ); +} + +export function convertGetParams({ allowCredentials, challenge, ...rest }) { + return { + ...rest, + ...(allowCredentials ? { allowCredentials: convertIdsToBuffer(allowCredentials) } : {}), + challenge: base64ToBuffer(challenge), + }; +} + +export function convertGetResponse(webauthnResponse) { + return { + type: webauthnResponse.type, + id: webauthnResponse.id, + rawId: bufferToBase64(webauthnResponse.rawId), + response: convertPropertiesToBase64(webauthnResponse.response, [ + 'clientDataJSON', + 'authenticatorData', + 'signature', + 'userHandle', + ]), + clientExtensionResults: webauthnResponse.getClientExtensionResults(), + }; +} + +export function convertCreateParams({ challenge, user, excludeCredentials, ...rest }) { + return { + ...rest, + challenge: base64ToBuffer(challenge), + user: convertIdToBuffer(user), + ...(excludeCredentials ? { excludeCredentials: convertIdsToBuffer(excludeCredentials) } : {}), + }; +} + +export function convertCreateResponse(webauthnResponse) { + return { + type: webauthnResponse.type, + id: webauthnResponse.id, + rawId: bufferToBase64(webauthnResponse.rawId), + clientExtensionResults: webauthnResponse.getClientExtensionResults(), + response: convertPropertiesToBase64(webauthnResponse.response, [ + 'clientDataJSON', + 'attestationObject', + ]), + }; +} diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue index 3242993b06a..3dd05f73841 100644 --- a/app/assets/javascripts/badges/components/badge.vue +++ b/app/assets/javascripts/badges/components/badge.vue @@ -1,13 +1,12 @@ <script> -import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui'; export default { // name: 'Badge' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25 // eslint-disable-next-line @gitlab/require-i18n-strings name: 'Badge', components: { - Icon, + GlIcon, GlLoadingIcon, }, directives: { @@ -84,7 +83,7 @@ export default { <div v-show="hasError" class="btn-group"> <div class="btn btn-default btn-sm disabled"> - <icon :size="16" class="gl-ml-3 gl-mr-3" name="doc-image" aria-hidden="true" /> + <gl-icon :size="16" class="gl-ml-3 gl-mr-3" name="doc-image" aria-hidden="true" /> </div> <div class="btn btn-default btn-sm disabled"> <span class="gl-ml-3 gl-mr-3">{{ s__('Badges|No badge image') }}</span> @@ -99,7 +98,7 @@ export default { type="button" @click="reloadImage" > - <icon :size="16" name="retry" /> + <gl-icon :size="16" name="retry" /> </button> </div> </template> diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue index 08834df0a9b..6afb10dd2ad 100644 --- a/app/assets/javascripts/badges/components/badge_form.vue +++ b/app/assets/javascripts/badges/components/badge_form.vue @@ -1,10 +1,10 @@ <script> +/* eslint-disable vue/no-v-html */ import { escape, debounce } from 'lodash'; import { mapActions, mapState } from 'vuex'; -import { GlLoadingIcon, GlFormInput, GlFormGroup } from '@gitlab/ui'; +import { GlLoadingIcon, GlFormInput, GlFormGroup, GlButton } from '@gitlab/ui'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { s__, sprintf } from '~/locale'; -import LoadingButton from '~/vue_shared/components/loading_button.vue'; import createEmptyBadge from '../empty_badge'; import Badge from './badge.vue'; @@ -14,7 +14,7 @@ export default { name: 'BadgeForm', components: { Badge, - LoadingButton, + GlButton, GlLoadingIcon, GlFormInput, GlFormGroup, @@ -219,23 +219,23 @@ export default { </div> <div v-if="isEditing" class="row-content-block gl-display-flex gl-justify-content-end"> - <button class="btn btn-cancel gl-mr-4" type="button" @click="onCancel"> + <gl-button class="btn-cancel gl-mr-4" data-testid="cancelEditing" @click="onCancel"> {{ __('Cancel') }} - </button> - <loading-button + </gl-button> + <gl-button :loading="isSaving" - :label="s__('Badges|Save changes')" type="submit" - container-class="btn btn-success" - /> + variant="success" + category="primary" + data-testid="saveEditing" + > + {{ s__('Badges|Save changes') }} + </gl-button> </div> <div v-else class="gl-display-flex gl-justify-content-end form-group"> - <loading-button - :loading="isSaving" - :label="s__('Badges|Add badge')" - type="submit" - container-class="btn btn-success" - /> + <gl-button :loading="isSaving" type="submit" variant="success" category="primary"> + {{ s__('Badges|Add badge') }} + </gl-button> </div> </form> </template> diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue index bad14666bb2..3343634ecad 100644 --- a/app/assets/javascripts/badges/components/badge_list_row.vue +++ b/app/assets/javascripts/badges/components/badge_list_row.vue @@ -1,8 +1,7 @@ <script> import { mapActions, mapState } from 'vuex'; -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; import { s__ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; import { PROJECT_BADGE } from '../constants'; import Badge from './badge.vue'; @@ -10,7 +9,7 @@ export default { name: 'BadgeListRow', components: { Badge, - Icon, + GlIcon, GlLoadingIcon, }, props: { @@ -58,7 +57,7 @@ export default { type="button" @click="editBadge(badge)" > - <icon :size="16" :aria-label="__('Edit')" name="pencil" /> + <gl-icon :size="16" :aria-label="__('Edit')" name="pencil" /> </button> <button :disabled="badge.isDeleting" @@ -68,7 +67,7 @@ export default { data-target="#delete-badge-modal" @click="updateBadgeInModal(badge)" > - <icon :size="16" :aria-label="__('Delete')" name="remove" /> + <gl-icon :size="16" :aria-label="__('Delete')" name="remove" /> </button> <gl-loading-icon v-show="badge.isDeleting" :inline="true" /> </div> diff --git a/app/assets/javascripts/batch_comments/components/draft_note.vue b/app/assets/javascripts/batch_comments/components/draft_note.vue index 39c1b8decee..a6cd36caede 100644 --- a/app/assets/javascripts/batch_comments/components/draft_note.vue +++ b/app/assets/javascripts/batch_comments/components/draft_note.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { mapActions, mapGetters, mapState } from 'vuex'; import { GlButton } from '@gitlab/ui'; import NoteableNote from '~/notes/components/noteable_note.vue'; diff --git a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue index 7520cc2401b..2b37ed19176 100644 --- a/app/assets/javascripts/batch_comments/components/preview_dropdown.vue +++ b/app/assets/javascripts/batch_comments/components/preview_dropdown.vue @@ -1,16 +1,16 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui'; import { sprintf, n__ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; import DraftsCount from './drafts_count.vue'; import PublishButton from './publish_button.vue'; import PreviewItem from './preview_item.vue'; export default { components: { + GlButton, GlLoadingIcon, - Icon, + GlIcon, DraftsCount, PublishButton, PreviewItem, @@ -29,7 +29,7 @@ export default { watch: { showPreviewDropdown() { if (this.showPreviewDropdown && this.$refs.dropdown) { - this.$nextTick(() => this.$refs.dropdown.focus()); + this.$nextTick(() => this.$refs.dropdown.$el.focus()); } }, }, @@ -63,32 +63,35 @@ export default { show: showPreviewDropdown, }" > - <button + <gl-button ref="dropdown" type="button" - class="btn btn-success review-preview-dropdown-toggle qa-review-preview-toggle" + category="primary" + variant="success" + class="review-preview-dropdown-toggle qa-review-preview-toggle" @click="toggleReviewDropdown" > {{ __('Finish review') }} <drafts-count /> - <icon name="angle-up" /> - </button> + <gl-icon name="angle-up" /> + </gl-button> <div class="dropdown-menu dropdown-menu-large dropdown-menu-right dropdown-open-top" :class="{ show: showPreviewDropdown, }" > - <div class="dropdown-title"> - {{ dropdownTitle }} - <button + <div class="dropdown-title gl-display-flex gl-align-items-center"> + <span class="gl-ml-auto">{{ dropdownTitle }}</span> + <gl-button :aria-label="__('Close')" type="button" - class="dropdown-title-button dropdown-menu-close" + category="tertiary" + size="small" + class="dropdown-title-button gl-ml-auto gl-p-0!" + icon="close" @click="toggleReviewDropdown" - > - <icon name="close" /> - </button> + /> </div> <div class="dropdown-content"> <ul v-if="isNotesFetched"> diff --git a/app/assets/javascripts/batch_comments/components/preview_item.vue b/app/assets/javascripts/batch_comments/components/preview_item.vue index 982fb01f49a..c89a6b537ef 100644 --- a/app/assets/javascripts/batch_comments/components/preview_item.vue +++ b/app/assets/javascripts/batch_comments/components/preview_item.vue @@ -1,9 +1,8 @@ <script> import { mapActions, mapGetters } from 'vuex'; -import { GlSprintf } from '@gitlab/ui'; +import { GlSprintf, GlIcon } from '@gitlab/ui'; import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants'; import { sprintf, __ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; import resolvedStatusMixin from '../mixins/resolved_status'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { @@ -14,7 +13,7 @@ import { export default { components: { - Icon, + GlIcon, GlSprintf, }, mixins: [resolvedStatusMixin, glFeatureFlagsMixin()], @@ -101,7 +100,7 @@ export default { @click="scrollToDraft(draft)" > <span class="review-preview-item-header"> - <icon class="flex-shrink-0" :name="iconName" /> + <gl-icon class="flex-shrink-0" :name="iconName" /> <span class="bold text-nowrap" :class="{ 'gl-align-items-center': glFeatures.multilineComments }" @@ -138,7 +137,7 @@ export default { v-if="draft.discussion_id && resolvedStatusMessage" class="review-preview-item-footer draft-note-resolution p-0" > - <icon class="gl-mr-3" name="status_success" /> {{ resolvedStatusMessage }} + <gl-icon class="gl-mr-3" name="status_success" /> {{ resolvedStatusMessage }} </span> </button> </template> diff --git a/app/assets/javascripts/batch_comments/components/review_bar.vue b/app/assets/javascripts/batch_comments/components/review_bar.vue index 2d7b86d2431..e51888eabc1 100644 --- a/app/assets/javascripts/batch_comments/components/review_bar.vue +++ b/app/assets/javascripts/batch_comments/components/review_bar.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { mapActions, mapState, mapGetters } from 'vuex'; import { GlModal, GlModalDirective, GlButton } from '@gitlab/ui'; import { sprintf, s__ } from '~/locale'; diff --git a/app/assets/javascripts/batch_comments/index.js b/app/assets/javascripts/batch_comments/index.js index e06285c0b37..9c763e70d63 100644 --- a/app/assets/javascripts/batch_comments/index.js +++ b/app/assets/javascripts/batch_comments/index.js @@ -3,7 +3,6 @@ import { mapActions } from 'vuex'; import store from '~/mr_notes/stores'; import ReviewBar from './components/review_bar.vue'; -// eslint-disable-next-line import/prefer-default-export export const initReviewBar = () => { const el = document.getElementById('js-review-bar'); diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js index add43b81f6d..d61797b7ae4 100644 --- a/app/assets/javascripts/behaviors/autosize.js +++ b/app/assets/javascripts/behaviors/autosize.js @@ -1,8 +1,13 @@ import Autosize from 'autosize'; +import { waitForCSSLoaded } from '../helpers/startup_css_helper'; document.addEventListener('DOMContentLoaded', () => { - const autosizeEls = document.querySelectorAll('.js-autosize'); + waitForCSSLoaded(() => { + const autosizeEls = document.querySelectorAll('.js-autosize'); - Autosize(autosizeEls); - Autosize.update(autosizeEls); + Autosize(autosizeEls); + Autosize.update(autosizeEls); + + autosizeEls.forEach(el => el.classList.add('js-autosize-initialized')); + }); }); diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js index 8060938c72a..fd12c282b62 100644 --- a/app/assets/javascripts/behaviors/index.js +++ b/app/assets/javascripts/behaviors/index.js @@ -1,7 +1,7 @@ +import $ from 'jquery'; import './autosize'; import './bind_in_out'; import './markdown/render_gfm'; -import initGFMInput from './markdown/gfm_auto_complete'; import initCopyAsGFM from './markdown/copy_as_gfm'; import initCopyToClipboard from './copy_to_clipboard'; import './details_behavior'; @@ -15,9 +15,27 @@ import initCollapseSidebarOnWindowResize from './collapse_sidebar_on_window_resi import initSelect2Dropdowns from './select2'; installGlEmojiElement(); -initGFMInput(); + initCopyAsGFM(); initCopyToClipboard(); + initPageShortcuts(); initCollapseSidebarOnWindowResize(); initSelect2Dropdowns(); + +document.addEventListener('DOMContentLoaded', () => { + window.requestIdleCallback( + () => { + // Check if we have to Load GFM Input + const $gfmInputs = $('.js-gfm-input:not(.js-gfm-input-initialized)'); + if ($gfmInputs.length) { + import(/* webpackChunkName: 'initGFMInput' */ './markdown/gfm_auto_complete') + .then(({ default: initGFMInput }) => { + initGFMInput($gfmInputs); + }) + .catch(() => {}); + } + }, + { timeout: 500 }, + ); +}); diff --git a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js index 6bbd2133344..d712c90242c 100644 --- a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js +++ b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js @@ -2,8 +2,8 @@ import $ from 'jquery'; import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete'; import { parseBoolean } from '~/lib/utils/common_utils'; -export default function initGFMInput() { - $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => { +export default function initGFMInput($els) { + $els.each((i, el) => { const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); const enableGFM = parseBoolean(el.dataset.supportsAutocomplete); @@ -14,6 +14,7 @@ export default function initGFMInput() { milestones: enableGFM, mergeRequests: enableGFM, labels: enableGFM, + vulnerabilities: enableGFM, }); }); } diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js index 01627b7206d..5e9d80e1529 100644 --- a/app/assets/javascripts/behaviors/markdown/render_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js @@ -5,7 +5,6 @@ import renderMermaid from './render_mermaid'; import renderMetrics from './render_metrics'; import highlightCurrentUser from './highlight_current_user'; import initUserPopovers from '../../user_popovers'; -import initMRPopovers from '../../mr_popover'; // Render GitLab flavoured Markdown // @@ -17,9 +16,25 @@ $.fn.renderGFM = function renderGFM() { renderMermaid(this.find('.js-render-mermaid')); highlightCurrentUser(this.find('.gfm-project_member').get()); initUserPopovers(this.find('.js-user-link').get()); - initMRPopovers(this.find('.gfm-merge_request').get()); + + const mrPopoverElements = this.find('.gfm-merge_request').get(); + if (mrPopoverElements.length) { + import(/* webpackChunkName: 'MrPopoverBundle' */ '../../mr_popover') + .then(({ default: initMRPopovers }) => { + initMRPopovers(mrPopoverElements); + }) + .catch(() => {}); + } + renderMetrics(this.find('.js-render-metrics').get()); return this; }; -$(() => $('body').renderGFM()); +$(() => { + window.requestIdleCallback( + () => { + $('body').renderGFM(); + }, + { timeout: 500 }, + ); +}); diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js index 03d9955f8fc..30783562da9 100644 --- a/app/assets/javascripts/behaviors/markdown/render_math.js +++ b/app/assets/javascripts/behaviors/markdown/render_math.js @@ -1,5 +1,6 @@ import { deprecatedCreateFlash as flash } from '~/flash'; import { s__, sprintf } from '~/locale'; +import { differenceInMilliseconds } from '~/lib/utils/datetime_utility'; // Renders math using KaTeX in any element with the // `js-render-math` class @@ -111,7 +112,7 @@ class SafeMathRenderer { // Give the browser time to reflow the svg waitForReflow(() => { - const deltaTime = Date.now() - this.startTime; + const deltaTime = differenceInMilliseconds(this.startTime); this.totalMS += deltaTime; this.renderElement(); diff --git a/app/assets/javascripts/behaviors/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts.js index 7987a533ae5..7352be0dbd5 100644 --- a/app/assets/javascripts/behaviors/shortcuts.js +++ b/app/assets/javascripts/behaviors/shortcuts.js @@ -1,5 +1,3 @@ -import Shortcuts from './shortcuts/shortcuts'; - export default function initPageShortcuts() { const { page } = document.body.dataset; const pagesWithCustomShortcuts = [ @@ -29,7 +27,9 @@ export default function initPageShortcuts() { // the pages above have their own shortcuts sub-classes instantiated elsewhere // TODO: replace this whitelist with something more automated/maintainable if (page && !pagesWithCustomShortcuts.includes(page)) { - return new Shortcuts(); + import(/* webpackChunkName: 'shortcutsBundle' */ './shortcuts/shortcuts') + .then(({ default: Shortcuts }) => new Shortcuts()) + .catch(() => {}); } return false; } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js index 85636f3e5d2..8a8b61a57cd 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import Cookies from 'js-cookie'; import Mousetrap from 'mousetrap'; import Vue from 'vue'; +import { flatten } from 'lodash'; import { disableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle'; import ShortcutsToggle from './shortcuts_toggle.vue'; import axios from '../../lib/utils/axios_utils'; @@ -9,13 +10,13 @@ import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility'; import findAndFollowLink from '../../lib/utils/navigation_utility'; import { parseBoolean, getCspNonceValue } from '~/lib/utils/common_utils'; -const defaultStopCallback = Mousetrap.stopCallback; -Mousetrap.stopCallback = (e, element, combo) => { +const defaultStopCallback = Mousetrap.prototype.stopCallback; +Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo) { if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) { return false; } - return defaultStopCallback(e, element, combo); + return defaultStopCallback.call(this, e, element, combo); }; function initToggleButton() { @@ -27,6 +28,39 @@ function initToggleButton() { }); } +/** + * The key used to save and fetch the local Mousetrap instance + * attached to a `<textarea>` element using `jQuery.data` + */ +const LOCAL_MOUSETRAP_DATA_KEY = 'local-mousetrap-instance'; + +/** + * Gets a mapping of toolbar button => keyboard shortcuts + * associated to the given markdown editor `<textarea>` element + * + * @param {HTMLTextAreaElement} $textarea The jQuery-wrapped `<textarea>` + * element to extract keyboard shortcuts from + * + * @returns A Map with keys that are jQuery-wrapped toolbar buttons + * (i.e. `$toolbarBtn`) and values that are arrays of string + * keyboard shortcuts (e.g. `['command+k', 'ctrl+k]`). + */ +function getToolbarBtnToShortcutsMap($textarea) { + const $allToolbarBtns = $textarea.closest('.md-area').find('.js-md'); + const map = new Map(); + + $allToolbarBtns.each(function attachToolbarBtnHandler() { + const $toolbarBtn = $(this); + const keyboardShortcuts = $toolbarBtn.data('md-shortcuts'); + + if (keyboardShortcuts?.length) { + map.set($toolbarBtn, keyboardShortcuts); + } + }); + + return map; +} + export default class Shortcuts { constructor() { this.onToggleHelp = this.onToggleHelp.bind(this); @@ -34,6 +68,7 @@ export default class Shortcuts { Mousetrap.bind('?', this.onToggleHelp); Mousetrap.bind('s', Shortcuts.focusSearch); + Mousetrap.bind('/', Shortcuts.focusSearch); Mousetrap.bind('f', this.focusFilter.bind(this)); Mousetrap.bind('p b', Shortcuts.onTogglePerfBar); @@ -143,4 +178,62 @@ export default class Shortcuts { e.preventDefault(); } } + + /** + * Initializes markdown editor shortcuts on the provided `<textarea>` element + * + * @param {JQuery} $textarea The jQuery-wrapped `<textarea>` element + * where markdown shortcuts should be enabled + * @param {Function} handler The handler to call when a + * keyboard shortcut is pressed inside the markdown `<textarea>` + */ + static initMarkdownEditorShortcuts($textarea, handler) { + const toolbarBtnToShortcutsMap = getToolbarBtnToShortcutsMap($textarea); + + const localMousetrap = new Mousetrap($textarea[0]); + + // Save a reference to the local mousetrap instance on the <textarea> + // so that it can be retrieved when unbinding shortcut handlers + $textarea.data(LOCAL_MOUSETRAP_DATA_KEY, localMousetrap); + + toolbarBtnToShortcutsMap.forEach((keyboardShortcuts, $toolbarBtn) => { + localMousetrap.bind(keyboardShortcuts, e => { + e.preventDefault(); + + handler($toolbarBtn); + }); + }); + + // Get an array of all shortcut strings that have been added above + const allShortcuts = flatten([...toolbarBtnToShortcutsMap.values()]); + + const originalStopCallback = Mousetrap.prototype.stopCallback; + localMousetrap.stopCallback = function newStopCallback(e, element, combo) { + if (allShortcuts.includes(combo)) { + return false; + } + + return originalStopCallback.call(this, e, element, combo); + }; + } + + /** + * Removes markdown editor shortcut handlers originally attached + * with `initMarkdownEditorShortcuts`. + * + * Note: it is safe to call this function even if `initMarkdownEditorShortcuts` + * has _not_ yet been called on the given `<textarea>`. + * + * @param {JQuery} $textarea The jQuery-wrapped `<textarea>` + * to remove shortcut handlers from + */ + static removeMarkdownEditorShortcuts($textarea) { + const localMousetrap = $textarea.data(LOCAL_MOUSETRAP_DATA_KEY); + + if (localMousetrap) { + getToolbarBtnToShortcutsMap($textarea).forEach(keyboardShortcuts => { + localMousetrap.unbind(keyboardShortcuts); + }); + } + } } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js index 8658081c6c2..f0d2ecfd210 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js @@ -5,12 +5,11 @@ export default class ShortcutsFindFile extends ShortcutsNavigation { constructor(projectFindFile) { super(); - const oldStopCallback = Mousetrap.stopCallback; - this.projectFindFile = projectFindFile; + const oldStopCallback = Mousetrap.prototype.stopCallback; - Mousetrap.stopCallback = (e, element, combo) => { + Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo) { if ( - element === this.projectFindFile.inputElement[0] && + element === projectFindFile.inputElement[0] && (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter') ) { // when press up/down key in textbox, cursor prevent to move to home/end @@ -18,12 +17,12 @@ export default class ShortcutsFindFile extends ShortcutsNavigation { return false; } - return oldStopCallback(e, element, combo); + return oldStopCallback.call(this, e, element, combo); }; - Mousetrap.bind('up', this.projectFindFile.selectRowUp); - Mousetrap.bind('down', this.projectFindFile.selectRowDown); - Mousetrap.bind('esc', this.projectFindFile.goToTree); - Mousetrap.bind('enter', this.projectFindFile.goToBlob); + Mousetrap.bind('up', projectFindFile.selectRowUp); + Mousetrap.bind('down', projectFindFile.selectRowDown); + Mousetrap.bind('esc', projectFindFile.goToTree); + Mousetrap.bind('enter', projectFindFile.goToBlob); } } diff --git a/app/assets/javascripts/blob/components/blob_edit_content.vue b/app/assets/javascripts/blob/components/blob_edit_content.vue index 26ba7b98a39..6293f3bed1c 100644 --- a/app/assets/javascripts/blob/components/blob_edit_content.vue +++ b/app/assets/javascripts/blob/components/blob_edit_content.vue @@ -46,7 +46,7 @@ export default { blobGlobalId: this.fileGlobalId, }); - this.editor.onChangeContent(debounce(this.onFileChange.bind(this), 250)); + this.editor.onDidChangeModelContent(debounce(this.onFileChange.bind(this), 250)); window.requestAnimationFrame(() => { if (!performance.getEntriesByName(SNIPPET_MARK_BLOBS_CONTENT).length) { diff --git a/app/assets/javascripts/blob/components/blob_embeddable.vue b/app/assets/javascripts/blob/components/blob_embeddable.vue deleted file mode 100644 index 00b915ec8bd..00000000000 --- a/app/assets/javascripts/blob/components/blob_embeddable.vue +++ /dev/null @@ -1,41 +0,0 @@ -<script> -import { GlFormInputGroup, GlDeprecatedButton, GlIcon } from '@gitlab/ui'; -import { __ } from '~/locale'; - -export default { - components: { - GlFormInputGroup, - GlDeprecatedButton, - GlIcon, - }, - props: { - url: { - type: String, - required: true, - }, - }, - data() { - return { - optionValues: [ - // eslint-disable-next-line no-useless-escape - { name: __('Embed'), value: `<script src='${this.url}.js'><\/script>` }, - { name: __('Share'), value: this.url }, - ], - }; - }, -}; -</script> -<template> - <gl-form-input-group - id="embeddable-text" - :predefined-options="optionValues" - readonly - select-on-click - > - <template #append> - <gl-deprecated-button new-style data-clipboard-target="#embeddable-text"> - <gl-icon name="copy-to-clipboard" :title="__('Copy')" /> - </gl-deprecated-button> - </template> - </gl-form-input-group> -</template> diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js index 4409d7a33cc..5058ca7122d 100644 --- a/app/assets/javascripts/blob/file_template_mediator.js +++ b/app/assets/javascripts/blob/file_template_mediator.js @@ -1,16 +1,18 @@ import $ from 'jquery'; + import Api from '~/api'; +import toast from '~/vue_shared/plugins/global_toast'; +import { __ } from '~/locale'; +import initPopover from '~/blob/suggest_gitlab_ci_yml'; import { deprecatedCreateFlash as Flash } from '../flash'; + import FileTemplateTypeSelector from './template_selectors/type_selector'; import BlobCiYamlSelector from './template_selectors/ci_yaml_selector'; import DockerfileSelector from './template_selectors/dockerfile_selector'; import GitignoreSelector from './template_selectors/gitignore_selector'; import LicenseSelector from './template_selectors/license_selector'; import MetricsDashboardSelector from './template_selectors/metrics_dashboard_selector'; -import toast from '~/vue_shared/plugins/global_toast'; -import { __ } from '~/locale'; -import initPopover from '~/blob/suggest_gitlab_ci_yml'; export default class FileTemplateMediator { constructor({ editor, currentAction, projectId }) { diff --git a/app/assets/javascripts/blob/file_template_selector.js b/app/assets/javascripts/blob/file_template_selector.js index 476901aae75..bd39aa2e16f 100644 --- a/app/assets/javascripts/blob/file_template_selector.js +++ b/app/assets/javascripts/blob/file_template_selector.js @@ -45,11 +45,15 @@ export default class FileTemplateSelector { } renderLoading() { - this.$loadingIcon.addClass('fa-spinner fa-spin').removeClass('fa-chevron-down'); + this.$loadingIcon + .addClass('gl-spinner gl-spinner-orange gl-spinner-sm') + .removeClass('fa-chevron-down'); } renderLoaded() { - this.$loadingIcon.addClass('fa-chevron-down').removeClass('fa-spinner fa-spin'); + this.$loadingIcon + .addClass('fa-chevron-down') + .removeClass('gl-spinner gl-spinner-orange gl-spinner-sm'); } reportSelection(options) { diff --git a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue index 90eafb75758..411241b72d5 100644 --- a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue +++ b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue @@ -1,5 +1,5 @@ <script> -import { GlModal, GlSprintf, GlLink } from '@gitlab/ui'; +import { GlModal, GlSprintf, GlLink, GlButton } from '@gitlab/ui'; import Cookies from 'js-cookie'; import { sprintf, s__, __ } from '~/locale'; import { glEmojiTag } from '~/emoji'; @@ -18,6 +18,8 @@ export default { helpMessage: s__( `MR widget|Take a look at our %{beginnerLinkStart}Beginner's Guide to Continuous Integration%{beginnerLinkEnd} and our %{exampleLinkStart}examples of GitLab CI/CD%{exampleLinkEnd} to learn more.`, ), + pipelinesButton: s__('MR widget|See your pipeline in action'), + mergeRequestButton: s__('MR widget|Back to the Merge request'), modalTitle: sprintf( __("That's it, well done!%{celebrate}"), { @@ -25,11 +27,13 @@ export default { }, false, ), - goToTrackValue: 10, + goToTrackValuePipelines: 10, + goToTrackValueMergeRequest: 20, trackEvent: 'click_button', components: { GlModal, GlSprintf, + GlButton, GlLink, }, mixins: [trackingMixin], @@ -38,6 +42,11 @@ export default { type: String, required: true, }, + projectMergeRequestsPath: { + type: String, + required: false, + default: '', + }, commitCookie: { type: String, required: true, @@ -59,6 +68,15 @@ export default { property: this.humanAccess, }; }, + goToMergeRequestPath() { + return this.commitCookiePath || this.projectMergeRequestsPath; + }, + commitCookiePath() { + const cookieVal = Cookies.get(this.commitCookie); + + if (cookieVal !== 'true') return cookieVal; + return ''; + }, }, mounted() { this.track(); @@ -100,17 +118,28 @@ export default { </template> </gl-sprintf> <template #modal-footer> - <a - ref="goto" + <gl-button + v-if="projectMergeRequestsPath" + ref="goToMergeRequest" + :href="goToMergeRequestPath" + :data-track-property="humanAccess" + :data-track-value="$options.goToTrackValueMergeRequest" + :data-track-event="$options.trackEvent" + :data-track-label="trackLabel" + > + {{ $options.mergeRequestButton }} + </gl-button> + <gl-button + ref="goToPipelines" :href="goToPipelinesPath" - class="btn btn-success" + variant="success" :data-track-property="humanAccess" - :data-track-value="$options.goToTrackValue" + :data-track-value="$options.goToTrackValuePipelines" :data-track-event="$options.trackEvent" :data-track-label="trackLabel" > - {{ __('See your pipeline in action') }} - </a> + {{ $options.pipelinesButton }} + </gl-button> </template> </gl-modal> </template> diff --git a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue index aff6a56cb0b..06f436adb8e 100644 --- a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue +++ b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue @@ -49,6 +49,10 @@ export default { type: String, required: true, }, + mergeRequestPath: { + type: String, + required: true, + }, }, data() { return { @@ -109,7 +113,7 @@ export default { :css-classes="['suggest-gitlab-ci-yml', 'ml-4']" > <template #title> - <span v-html="suggestTitle"></span> + <span>{{ suggestTitle }}</span> <span class="ml-auto"> <gl-button :aria-label="__('Close')" 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 3b67b3dd259..55edb852ee6 100644 --- a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/index.js +++ b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/index.js @@ -10,6 +10,7 @@ export default el => target: el.dataset.target, trackLabel: el.dataset.trackLabel, dismissKey: el.dataset.dismissKey, + mergeRequestPath: el.dataset.mergeRequestPath, humanAccess: el.dataset.humanAccess, }, }); diff --git a/app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue b/app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue new file mode 100644 index 00000000000..1308ca53e74 --- /dev/null +++ b/app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue @@ -0,0 +1,50 @@ +<script> +import { GlAlert, GlButton } from '@gitlab/ui'; +import axios from '~/lib/utils/axios_utils'; + +export default { + components: { + GlAlert, + GlButton, + }, + props: { + dismissEndpoint: { + type: String, + required: true, + }, + featureId: { + type: String, + required: true, + }, + editPath: { + type: String, + required: true, + }, + }, + data() { + return { + showAlert: true, + }; + }, + methods: { + dismissAlert() { + this.showAlert = false; + + return axios.post(this.dismissEndpoint, { + feature_name: this.featureId, + }); + }, + }, +}; +</script> + +<template> + <gl-alert v-if="showAlert" class="gl-mt-5" @dismiss="dismissAlert"> + {{ __('The Web IDE offers advanced syntax highlighting capabilities and more.') }} + <div class="gl-mt-5"> + <gl-button :href="editPath" category="primary" variant="info">{{ + __('Open Web IDE') + }}</gl-button> + </div> + </gl-alert> +</template> diff --git a/app/assets/javascripts/blob/suggest_web_ide_ci/index.js b/app/assets/javascripts/blob/suggest_web_ide_ci/index.js new file mode 100644 index 00000000000..eadf3cd6216 --- /dev/null +++ b/app/assets/javascripts/blob/suggest_web_ide_ci/index.js @@ -0,0 +1,20 @@ +import Vue from 'vue'; +import WebIdeAlert from './components/web_ide_alert.vue'; + +export default el => { + const { dismissEndpoint, featureId, editPath } = el.dataset; + + // eslint-disable-next-line no-new + new Vue({ + el, + render(createElement) { + return createElement(WebIdeAlert, { + props: { + dismissEndpoint, + featureId, + editPath, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js index 2427e25a17d..257458138dc 100644 --- a/app/assets/javascripts/blob/template_selector.js +++ b/app/assets/javascripts/blob/template_selector.js @@ -1,7 +1,7 @@ /* eslint-disable class-methods-use-this */ import $ from 'jquery'; -import '~/gl_dropdown'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default class TemplateSelector { constructor({ dropdown, data, pattern, wrapper, editor, $input } = {}) { @@ -19,7 +19,7 @@ export default class TemplateSelector { } initDropdown(dropdown, data) { - return $(dropdown).glDropdown({ + return initDeprecatedJQueryDropdown($(dropdown), { data, filterable: true, selectable: true, 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 d819452df68..3a4e86fe572 100644 --- a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js +++ b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js @@ -1,4 +1,5 @@ import FileTemplateSelector from '../file_template_selector'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default class BlobCiYamlSelector extends FileTemplateSelector { constructor({ mediator }) { @@ -15,7 +16,7 @@ export default class BlobCiYamlSelector extends FileTemplateSelector { initDropdown() { // maybe move to super class as well - this.$dropdown.glDropdown({ + initDeprecatedJQueryDropdown(this.$dropdown, { data: this.$dropdown.data('data'), filterable: true, selectable: true, diff --git a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js index 7d5e98889d3..3cb4bb83930 100644 --- a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js +++ b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js @@ -1,5 +1,6 @@ import FileTemplateSelector from '../file_template_selector'; import { __ } from '~/locale'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default class DockerfileSelector extends FileTemplateSelector { constructor({ mediator }) { @@ -16,7 +17,7 @@ export default class DockerfileSelector extends FileTemplateSelector { initDropdown() { // maybe move to super class as well - this.$dropdown.glDropdown({ + initDeprecatedJQueryDropdown(this.$dropdown, { data: this.$dropdown.data('data'), filterable: true, selectable: true, diff --git a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js index 39a8937641d..1721230dcb7 100644 --- a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js +++ b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js @@ -1,4 +1,5 @@ import FileTemplateSelector from '../file_template_selector'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default class BlobGitignoreSelector extends FileTemplateSelector { constructor({ mediator }) { @@ -14,7 +15,7 @@ export default class BlobGitignoreSelector extends FileTemplateSelector { } initDropdown() { - this.$dropdown.glDropdown({ + initDeprecatedJQueryDropdown(this.$dropdown, { data: this.$dropdown.data('data'), filterable: true, selectable: true, diff --git a/app/assets/javascripts/blob/template_selectors/license_selector.js b/app/assets/javascripts/blob/template_selectors/license_selector.js index f4041835a7d..dafde82b1e0 100644 --- a/app/assets/javascripts/blob/template_selectors/license_selector.js +++ b/app/assets/javascripts/blob/template_selectors/license_selector.js @@ -1,4 +1,5 @@ import FileTemplateSelector from '../file_template_selector'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default class BlobLicenseSelector extends FileTemplateSelector { constructor({ mediator }) { @@ -14,7 +15,7 @@ export default class BlobLicenseSelector extends FileTemplateSelector { } initDropdown() { - this.$dropdown.glDropdown({ + initDeprecatedJQueryDropdown(this.$dropdown, { data: this.$dropdown.data('data'), filterable: true, selectable: true, 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 b4accaadfa3..9e698bfea5d 100644 --- a/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js +++ b/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js @@ -1,4 +1,5 @@ import FileTemplateSelector from '../file_template_selector'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default class MetricsDashboardSelector extends FileTemplateSelector { constructor({ mediator }) { @@ -14,7 +15,7 @@ export default class MetricsDashboardSelector extends FileTemplateSelector { } initDropdown() { - this.$dropdown.glDropdown({ + initDeprecatedJQueryDropdown(this.$dropdown, { data: this.$dropdown.data('data'), filterable: true, selectable: true, diff --git a/app/assets/javascripts/blob/template_selectors/type_selector.js b/app/assets/javascripts/blob/template_selectors/type_selector.js index cb4e1aaa9ac..01625911815 100644 --- a/app/assets/javascripts/blob/template_selectors/type_selector.js +++ b/app/assets/javascripts/blob/template_selectors/type_selector.js @@ -1,4 +1,5 @@ import FileTemplateSelector from '../file_template_selector'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default class FileTemplateTypeSelector extends FileTemplateSelector { constructor({ mediator, dropdownData }) { @@ -12,7 +13,7 @@ export default class FileTemplateTypeSelector extends FileTemplateSelector { } initDropdown() { - this.$dropdown.glDropdown({ + initDeprecatedJQueryDropdown(this.$dropdown, { data: this.config.dropdownData, filterable: false, selectable: true, diff --git a/app/assets/javascripts/blob/utils.js b/app/assets/javascripts/blob/utils.js index a0211c8bb8e..8043c0bbc07 100644 --- a/app/assets/javascripts/blob/utils.js +++ b/app/assets/javascripts/blob/utils.js @@ -1,20 +1,16 @@ import Editor from '~/editor/editor_lite'; export function initEditorLite({ el, ...args }) { - if (!el) { - throw new Error(`"el" parameter is required to initialize Editor`); - } const editor = new Editor({ scrollbar: { alwaysConsumeMouseWheel: false, }, }); - editor.createInstance({ + + return editor.createInstance({ el, ...args, }); - - return editor; } export default () => ({}); diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index 9b9ade28623..c9972f0b43c 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -7,12 +7,14 @@ import BlobFileDropzone from '../blob/blob_file_dropzone'; import initPopover from '~/blob/suggest_gitlab_ci_yml'; import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils'; import Tracking from '~/tracking'; +import initWebIdeAlert from '~/blob/suggest_web_ide_ci'; export default () => { const editBlobForm = $('.js-edit-blob-form'); const uploadBlobForm = $('.js-upload-blob-form'); const deleteBlobForm = $('.js-delete-blob-form'); const suggestEl = document.querySelector('.js-suggest-gitlab-ci-yml'); + const alertEl = document.getElementById('js-suggest-web-ide-ci'); if (editBlobForm.length) { const urlRoot = editBlobForm.data('relativeUrlRoot'); @@ -65,12 +67,15 @@ export default () => { if (commitButton) { const { dismissKey, humanAccess } = suggestEl.dataset; + const urlParams = new URLSearchParams(window.location.search); + const mergeRequestPath = urlParams.get('mr_path') || true; + const commitCookieName = `suggest_gitlab_ci_yml_commit_${dismissKey}`; const commitTrackLabel = 'suggest_gitlab_ci_yml_commit_changes'; const commitTrackValue = '20'; commitButton.addEventListener('click', () => { - setCookie(commitCookieName, true); + setCookie(commitCookieName, mergeRequestPath); Tracking.event(undefined, 'click_button', { label: commitTrackLabel, @@ -80,4 +85,8 @@ export default () => { }); } } + + if (alertEl) { + initWebIdeAlert(alertEl); + } }; diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index e22c9b0d4c4..2a4ab4b8827 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -40,9 +40,10 @@ export default class EditBlob { const MarkdownExtensionPromise = this.options.isMarkdown ? import('~/editor/editor_markdown_ext') : Promise.resolve(false); + const FileTemplateExtensionPromise = import('~/editor/editor_file_template_ext'); - return Promise.all([EditorPromise, MarkdownExtensionPromise]) - .then(([EditorModule, MarkdownExtension]) => { + return Promise.all([EditorPromise, MarkdownExtensionPromise, FileTemplateExtensionPromise]) + .then(([EditorModule, MarkdownExtension, FileTemplateExtension]) => { const EditorLite = EditorModule.default; const editorEl = document.getElementById('editor'); const fileNameEl = @@ -50,18 +51,16 @@ export default class EditBlob { const fileContentEl = document.getElementById('file-content'); const form = document.querySelector('.js-edit-blob-form'); - this.editor = new EditorLite(); + const rootEditor = new EditorLite(); - if (MarkdownExtension) { - this.editor.use(MarkdownExtension.default); - } - - this.editor.createInstance({ + this.editor = rootEditor.createInstance({ el: editorEl, blobPath: fileNameEl.value, blobContent: editorEl.innerText, }); + rootEditor.use([MarkdownExtension.default, FileTemplateExtension.default], this.editor); + fileNameEl.addEventListener('change', () => { this.editor.updateModelLanguage(fileNameEl.value); }); diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js index 384a386d69c..5c8df94ca90 100644 --- a/app/assets/javascripts/boards/boards_util.js +++ b/app/assets/javascripts/boards/boards_util.js @@ -1,28 +1,72 @@ +import { sortBy } from 'lodash'; import ListIssue from 'ee_else_ce/boards/models/issue'; +import { ListType } from './constants'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; export function getMilestone() { return null; } +export function formatIssue(issue) { + return new ListIssue({ + ...issue, + labels: issue.labels?.nodes || [], + assignees: issue.assignees?.nodes || [], + }); +} + export function formatListIssues(listIssues) { - return listIssues.nodes.reduce((map, list) => { + const issues = {}; + + const listData = listIssues.nodes.reduce((map, list) => { + const sortedIssues = sortBy(list.issues.nodes, 'relativePosition'); return { ...map, - [list.id]: list.issues.nodes.map( - i => - new ListIssue({ - ...i, - id: getIdFromGraphQLId(i.id), - labels: i.labels?.nodes || [], - assignees: i.assignees?.nodes || [], - }), - ), + [list.id]: sortedIssues.map(i => { + const id = getIdFromGraphQLId(i.id); + + const listIssue = new ListIssue({ + ...i, + id, + labels: i.labels?.nodes || [], + assignees: i.assignees?.nodes || [], + }); + + issues[id] = listIssue; + + return id; + }), }; }, {}); + + return { listData, issues }; +} + +export function fullBoardId(boardId) { + return `gid://gitlab/Board/${boardId}`; +} + +export function moveIssueListHelper(issue, fromList, toList) { + if (toList.type === ListType.label) { + issue.addLabel(toList.label); + } + if (fromList && fromList.type === ListType.label) { + issue.removeLabel(fromList.label); + } + + if (toList.type === ListType.assignee) { + issue.addAssignee(toList.assignee); + } + if (fromList && fromList.type === ListType.assignee) { + issue.removeAssignee(fromList.assignee); + } + + return issue; } export default { getMilestone, + formatIssue, formatListIssues, + fullBoardId, }; diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue index afdf0290e8e..55e3e4a6329 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.vue +++ b/app/assets/javascripts/boards/components/board_blank_state.vue @@ -1,10 +1,14 @@ <script> +import { GlButton } from '@gitlab/ui'; import Cookies from 'js-cookie'; import { __ } from '~/locale'; import ListLabel from '~/boards/models/label'; import boardsStore from '../stores/boards_store'; export default { + components: { + GlButton, + }, data() { return { predefinedLabels: [ @@ -84,15 +88,17 @@ export default { ) }} </p> - <button - class="btn btn-success btn-inverted btn-block" - type="button" + <gl-button + category="secondary" + variant="success" + block="block" + class="gl-mb-0" @click.stop="addDefaultLists" > {{ s__('BoardBlankState|Add default lists') }} - </button> - <button class="btn btn-default btn-block" type="button" @click.stop="clearBlankState"> + </gl-button> + <gl-button category="secondary" variant="default" block="block" @click.stop="clearBlankState"> {{ s__("BoardBlankState|Nevermind, I'll use my own") }} - </button> + </gl-button> </div> </template> diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 246d3b9dcd1..31050eef83d 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -1,6 +1,5 @@ <script> -/* eslint-disable vue/require-default-prop */ -import IssueCardInner from './issue_card_inner.vue'; +import BoardCardLayout from './board_card_layout.vue'; import eventHub from '../eventhub'; import sidebarEventHub from '~/sidebar/event_hub'; import boardsStore from '../stores/boards_store'; @@ -8,7 +7,7 @@ import boardsStore from '../stores/boards_store'; export default { name: 'BoardsIssueCard', components: { - IssueCardInner, + BoardCardLayout, }, props: { list: { @@ -21,80 +20,29 @@ export default { default: () => ({}), required: false, }, - issueLinkBase: { - type: String, - default: '', - required: false, - }, - disabled: { - type: Boolean, - default: false, - required: false, - }, - index: { - type: Number, - default: 0, - required: false, - }, - rootPath: { - type: String, - default: '', - required: false, - }, - groupId: { - type: Number, - required: false, - }, - }, - data() { - return { - showDetail: false, - detailIssue: boardsStore.detail, - multiSelect: boardsStore.multiSelect, - }; - }, - computed: { - issueDetailVisible() { - return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id; - }, - multiSelectVisible() { - return this.multiSelect.list.findIndex(issue => issue.id === this.issue.id) > -1; - }, - canMultiSelect() { - return gon.features && gon.features.multiSelectBoard; - }, }, methods: { - mouseDown() { - this.showDetail = true; + // These are methods instead of computed's, because boardsStore is not reactive. + isActive() { + return this.getActiveId() === this.issue.id; }, - mouseMove() { - this.showDetail = false; + getActiveId() { + return boardsStore.detail?.issue?.id; }, - showIssue(e) { - if (e.target.classList.contains('js-no-trigger')) return; - + showIssue({ isMultiSelect }) { // If no issues are opened, close all sidebars first - if (!boardsStore.detail?.issue?.id) { + if (!this.getActiveId()) { sidebarEventHub.$emit('sidebar.closeAll'); } + if (this.isActive()) { + eventHub.$emit('clearDetailIssue', isMultiSelect); - // If CMD or CTRL is clicked - const isMultiSelect = this.canMultiSelect && (e.ctrlKey || e.metaKey); - - if (this.showDetail || isMultiSelect) { - this.showDetail = false; - - if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) { - eventHub.$emit('clearDetailIssue', isMultiSelect); - - if (isMultiSelect) { - eventHub.$emit('newDetailIssue', this.issue, isMultiSelect); - } - } else { + if (isMultiSelect) { eventHub.$emit('newDetailIssue', this.issue, isMultiSelect); - boardsStore.setListDetail(this.list); } + } else { + eventHub.$emit('newDetailIssue', this.issue, isMultiSelect); + boardsStore.setListDetail(this.list); } }, }, @@ -102,28 +50,12 @@ export default { </script> <template> - <li - :class="{ - 'multi-select': multiSelectVisible, - 'user-can-drag': !disabled && issue.id, - 'is-disabled': disabled || !issue.id, - 'is-active': issueDetailVisible, - }" - :index="index" - :data-issue-id="issue.id" + <board-card-layout data-qa-selector="board_card" - class="board-card p-3 rounded" - @mousedown="mouseDown" - @mousemove="mouseMove" - @mouseup="showIssue($event)" - > - <issue-card-inner - :list="list" - :issue="issue" - :issue-link-base="issueLinkBase" - :group-id="groupId" - :root-path="rootPath" - :update-filters="true" - /> - </li> + :issue="issue" + :list="list" + :is-active="isActive()" + v-bind="$attrs" + @show="showIssue" + /> </template> diff --git a/app/assets/javascripts/boards/components/board_card_layout.vue b/app/assets/javascripts/boards/components/board_card_layout.vue new file mode 100644 index 00000000000..072dd87861a --- /dev/null +++ b/app/assets/javascripts/boards/components/board_card_layout.vue @@ -0,0 +1,93 @@ +<script> +import IssueCardInner from './issue_card_inner.vue'; +import boardsStore from '../stores/boards_store'; + +export default { + name: 'BoardsIssueCard', + components: { + IssueCardInner, + }, + props: { + list: { + type: Object, + default: () => ({}), + required: false, + }, + issue: { + type: Object, + default: () => ({}), + required: false, + }, + disabled: { + type: Boolean, + default: false, + required: false, + }, + index: { + type: Number, + default: 0, + required: false, + }, + isActive: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + showDetail: false, + multiSelect: boardsStore.multiSelect, + }; + }, + computed: { + multiSelectVisible() { + return this.multiSelect.list.findIndex(issue => issue.id === this.issue.id) > -1; + }, + canMultiSelect() { + return gon.features && gon.features.multiSelectBoard; + }, + }, + methods: { + mouseDown() { + this.showDetail = true; + }, + mouseMove() { + this.showDetail = false; + }, + showIssue(e) { + // Don't do anything if this happened on a no trigger element + if (e.target.classList.contains('js-no-trigger')) return; + + const isMultiSelect = this.canMultiSelect && (e.ctrlKey || e.metaKey); + + if (this.showDetail || isMultiSelect) { + this.showDetail = false; + this.$emit('show', { event: e, isMultiSelect }); + } + }, + }, +}; +</script> + +<template> + <li + :class="{ + 'multi-select': multiSelectVisible, + 'user-can-drag': !disabled && issue.id, + 'is-disabled': disabled || !issue.id, + 'is-active': isActive, + }" + :index="index" + :data-issue-id="issue.id" + :data-issue-iid="issue.iid" + :data-issue-path="issue.referencePath" + data-testid="board_card" + class="board-card p-3 rounded" + @mousedown="mouseDown" + @mousemove="mouseMove" + @mouseup="showIssue($event)" + > + <issue-card-inner :list="list" :issue="issue" :update-filters="true" /> + </li> +</template> diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue index dae24338e45..6d216911798 100644 --- a/app/assets/javascripts/boards/components/board_column.vue +++ b/app/assets/javascripts/boards/components/board_column.vue @@ -1,9 +1,11 @@ <script> +import { mapGetters, mapActions } from 'vuex'; import Sortable from 'sortablejs'; import isWipLimitsOn from 'ee_else_ce/boards/mixins/is_wip_limits'; import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue'; import Tooltip from '~/vue_shared/directives/tooltip'; import EmptyComponent from '~/vue_shared/components/empty_component'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import BoardBlankState from './board_blank_state.vue'; import BoardList from './board_list.vue'; import boardsStore from '../stores/boards_store'; @@ -21,7 +23,7 @@ export default { directives: { Tooltip, }, - mixins: [isWipLimitsOn], + mixins: [isWipLimitsOn, glFeatureFlagMixin()], props: { list: { type: Object, @@ -32,27 +34,15 @@ export default { type: Boolean, required: true, }, - issueLinkBase: { - type: String, - required: true, - }, - rootPath: { - type: String, - required: true, - }, - boardId: { - type: String, - required: true, - }, canAdminList: { type: Boolean, required: false, default: false, }, - groupId: { - type: Number, - required: false, - default: null, + }, + inject: { + boardId: { + type: String, }, }, data() { @@ -62,6 +52,7 @@ export default { }; }, computed: { + ...mapGetters(['getIssues']), showBoardListAndBoardInfo() { return this.list.type !== ListType.blank && this.list.type !== ListType.promotion; }, @@ -69,19 +60,36 @@ export default { // eslint-disable-next-line @gitlab/require-i18n-strings return `boards.${this.boardId}.${this.list.type}.${this.list.id}`; }, + listIssues() { + if (!this.glFeatures.graphqlBoardLists) { + return this.list.issues; + } + return this.getIssues(this.list.id); + }, + shouldFetchIssues() { + return this.glFeatures.graphqlBoardLists && this.list.type !== ListType.blank; + }, }, watch: { filter: { handler() { - this.list.page = 1; - this.list.getIssues(true).catch(() => { - // TODO: handle request error - }); + if (this.shouldFetchIssues) { + this.fetchIssuesForList(this.list.id); + } else { + this.list.page = 1; + this.list.getIssues(true).catch(() => { + // TODO: handle request error + }); + } }, deep: true, }, }, mounted() { + if (this.shouldFetchIssues) { + this.fetchIssuesForList(this.list.id); + } + const instance = this; const sortableOptions = getBoardSortableDefaultOptions({ @@ -108,6 +116,7 @@ export default { Sortable.create(this.$el.parentNode, sortableOptions); }, methods: { + ...mapActions(['fetchIssuesForList']), showListNewIssueForm(listId) { eventHub.$emit('showForm', listId); }, @@ -130,22 +139,14 @@ export default { <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-id="boardId" - /> + <board-list-header :can-admin-list="canAdminList" :list="list" :disabled="disabled" /> <board-list v-if="showBoardListAndBoardInfo" ref="board-list" :disabled="disabled" - :group-id="groupId || null" - :issue-link-base="issueLinkBase" - :issues="list.issues" + :issues="listIssues" :list="list" :loading="list.loading" - :root-path="rootPath" /> <board-blank-state v-if="canAdminList && list.id === 'blank'" /> diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue index c42295792f1..c7b3da0e672 100644 --- a/app/assets/javascripts/boards/components/board_content.vue +++ b/app/assets/javascripts/boards/components/board_content.vue @@ -1,13 +1,15 @@ <script> -import { mapState } from 'vuex'; +import { mapState, mapGetters, mapActions } from 'vuex'; import BoardColumn from 'ee_else_ce/boards/components/board_column.vue'; -import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue'; +import { GlAlert } from '@gitlab/ui'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { components: { BoardColumn, - EpicsSwimlanes, + BoardContentSidebar: () => import('ee_component/boards/components/board_content_sidebar.vue'), + EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'), + GlAlert, }, mixins: [glFeatureFlagMixin()], props: { @@ -19,66 +21,58 @@ export default { type: Boolean, required: true, }, - groupId: { - type: Number, - required: false, - default: null, - }, disabled: { type: Boolean, required: true, }, - issueLinkBase: { - type: String, - required: true, - }, - rootPath: { - type: String, - required: true, - }, - boardId: { - type: String, - required: true, - }, }, computed: { - ...mapState(['isShowingEpicsSwimlanes', 'boardLists']), - isSwimlanesOn() { - return this.glFeatures.boardsWithSwimlanes && this.isShowingEpicsSwimlanes; + ...mapState(['boardLists', 'error']), + ...mapGetters(['isSwimlanesOn']), + boardListsToUse() { + return this.glFeatures.graphqlBoardLists ? this.boardLists : this.lists; }, }, + mounted() { + if (this.glFeatures.graphqlBoardLists) { + this.fetchLists(); + this.showPromotionList(); + } + }, + methods: { + ...mapActions(['fetchLists', 'showPromotionList']), + }, }; </script> <template> <div> + <gl-alert v-if="error" variant="danger" :dismissible="false"> + {{ error }} + </gl-alert> <div v-if="!isSwimlanesOn" class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap" data-qa-selector="boards_list" > <board-column - v-for="list in lists" + v-for="list in boardListsToUse" :key="list.id" ref="board" :can-admin-list="canAdminList" - :group-id="groupId" :list="list" :disabled="disabled" - :issue-link-base="issueLinkBase" - :root-path="rootPath" - :board-id="boardId" /> </div> - <epics-swimlanes - v-else - ref="swimlanes" - :lists="boardLists" - :can-admin-list="canAdminList" - :disabled="disabled" - :board-id="boardId" - :group-id="groupId" - :root-path="rootPath" - /> + + <template v-else> + <epics-swimlanes + ref="swimlanes" + :lists="boardLists" + :can-admin-list="canAdminList" + :disabled="disabled" + /> + <board-content-sidebar /> + </template> </div> </template> diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue index 231059b895e..385dd5fdc71 100644 --- a/app/assets/javascripts/boards/components/board_form.vue +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -25,11 +25,11 @@ export default { type: Boolean, required: true, }, - milestonePath: { + labelsPath: { type: String, required: true, }, - labelsPath: { + labelsWebUrl: { type: String, required: true, }, @@ -201,8 +201,8 @@ export default { :collapse-scope="isNewForm" :board="board" :can-admin-board="canAdminBoard" - :milestone-path="milestonePath" :labels-path="labelsPath" + :labels-web-url="labelsWebUrl" :enable-scoped-labels="enableScopedLabels" :project-id="projectId" :group-id="groupId" diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 1a26782f6f0..25f8ffca633 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -6,6 +6,7 @@ import boardCard from './board_card.vue'; import eventHub from '../eventhub'; import boardsStore from '../stores/boards_store'; import { sprintf, __ } from '~/locale'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { getBoardSortableDefaultOptions, @@ -24,12 +25,8 @@ export default { boardNewIssue, GlLoadingIcon, }, + mixins: [glFeatureFlagMixin()], props: { - groupId: { - type: Number, - required: false, - default: 0, - }, disabled: { type: Boolean, required: true, @@ -46,14 +43,6 @@ export default { type: Boolean, required: true, }, - issueLinkBase: { - type: String, - required: true, - }, - rootPath: { - type: String, - required: true, - }, }, data() { return { @@ -83,6 +72,7 @@ export default { deep: true, }, issues() { + if (this.glFeatures.graphqlBoardLists) return; this.$nextTick(() => { if ( this.scrollHeight() <= this.listHeight() && @@ -413,6 +403,8 @@ export default { this.showIssueForm = !this.showIssueForm; }, onScroll() { + if (this.glFeatures.graphqlBoardLists) return; + if (!this.list.loadingMore && this.scrollTop() > this.scrollHeight() - this.scrollOffset) { this.loadNextPage(); } @@ -430,11 +422,7 @@ export default { <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" - :group-id="groupId" - :list="list" - /> + <board-new-issue v-if="list.type !== 'closed' && showIssueForm" :list="list" /> <ul v-show="!loading" ref="list" @@ -450,9 +438,6 @@ export default { :index="index" :list="list" :issue="issue" - :issue-link-base="issueLinkBase" - :group-id="groupId" - :root-path="rootPath" :disabled="disabled" /> <li v-if="showCount" class="board-list-count text-center" data-issue-id="-1"> diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue index bafe07afb48..361fe252afb 100644 --- a/app/assets/javascripts/boards/components/board_list_header.vue +++ b/app/assets/javascripts/boards/components/board_list_header.vue @@ -1,4 +1,5 @@ <script> +import { mapActions } from 'vuex'; import { GlButton, GlButtonGroup, @@ -17,6 +18,7 @@ import boardsStore from '../stores/boards_store'; import eventHub from '../eventhub'; import { ListType } from '../constants'; import { isScopedLabel } from '~/lib/utils/common_utils'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { components: { @@ -32,7 +34,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, - mixins: [isWipLimitsOn], + mixins: [isWipLimitsOn, glFeatureFlagMixin()], props: { list: { type: Object, @@ -43,10 +45,6 @@ export default { type: Boolean, required: true, }, - boardId: { - type: String, - required: true, - }, canAdminList: { type: Boolean, required: false, @@ -58,6 +56,11 @@ export default { default: false, }, }, + inject: { + boardId: { + type: String, + }, + }, data() { return { weightFeatureAvailable: false, @@ -94,10 +97,11 @@ export default { showAssigneeListDetails() { return this.list.type === 'assignee' && (this.list.isExpanded || !this.isSwimlanesHeader); }, + issuesCount() { + return this.list.issuesSize; + }, issuesTooltipLabel() { - const { issuesSize } = this.list; - - return n__(`%d issue`, `%d issues`, issuesSize); + return n__(`%d issue`, `%d issues`, this.issuesCount); }, chevronTooltip() { return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand'); @@ -126,8 +130,12 @@ export default { collapsedTooltipTitle() { return this.listTitle || this.listAssignee; }, + shouldDisplaySwimlanes() { + return this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn; + }, }, methods: { + ...mapActions(['updateList']), showScopedLabels(label) { return boardsStore.scopedLabels.enabled && isScopedLabel(label); }, @@ -136,20 +144,28 @@ export default { eventHub.$emit(`toggle-issue-form-${this.list.id}`); }, toggleExpanded() { - if (this.list.isExpandable) { - this.list.isExpanded = !this.list.isExpanded; - - if (AccessorUtilities.isLocalStorageAccessSafe() && !this.isLoggedIn) { - localStorage.setItem(`${this.uniqueKey}.expanded`, this.list.isExpanded); - } + this.list.isExpanded = !this.list.isExpanded; - if (this.isLoggedIn) { - this.list.update(); - } + if (!this.isLoggedIn) { + this.addToLocalStorage(); + } else { + this.updateListFunction(); + } - // When expanding/collapsing, the tooltip on the caret button sometimes stays open. - // Close all tooltips manually to prevent dangling tooltips. - this.$root.$emit('bv::hide::tooltip'); + // When expanding/collapsing, the tooltip on the caret button sometimes stays open. + // Close all tooltips manually to prevent dangling tooltips. + this.$root.$emit('bv::hide::tooltip'); + }, + addToLocalStorage() { + if (AccessorUtilities.isLocalStorageAccessSafe()) { + localStorage.setItem(`${this.uniqueKey}.expanded`, this.list.isExpanded); + } + }, + updateListFunction() { + if (this.shouldDisplaySwimlanes || this.glFeatures.graphqlBoardLists) { + this.updateList({ listId: this.list.id, collapsed: !this.list.isExpanded }); + } else { + this.list.update(); } }, }, @@ -172,7 +188,7 @@ export default { <h3 :class="{ 'user-can-drag': !disabled && !list.preset, - 'gl-py-3': !list.isExpanded && !isSwimlanesHeader, + 'gl-py-3 gl-h-full': !list.isExpanded && !isSwimlanesHeader, 'gl-border-b-0': !list.isExpanded || isSwimlanesHeader, 'gl-py-2': !list.isExpanded && isSwimlanesHeader, }" @@ -288,7 +304,7 @@ export default { <gl-tooltip :target="() => $refs.issueCount" :title="issuesTooltipLabel" /> <span ref="issueCount" class="issue-count-badge-count"> <gl-icon class="gl-mr-2" name="issues" /> - <issue-count :issues-size="list.issuesSize" :max-issue-count="list.maxIssueCount" /> + <issue-count :issues-size="issuesCount" :max-issue-count="list.maxIssueCount" /> </span> <!-- The following is only true in EE. --> <template v-if="weightFeatureAvailable"> diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index 34e8438ba4c..348d485ff37 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -1,11 +1,13 @@ <script> import $ from 'jquery'; +import { mapActions, mapGetters } 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'; export default { name: 'BoardNewIssue', @@ -13,17 +15,18 @@ export default { ProjectSelect, GlButton, }, + mixins: [glFeatureFlagMixin()], props: { - groupId: { - type: Number, - required: false, - default: 0, - }, list: { type: Object, required: true, }, }, + inject: { + groupId: { + type: Number, + }, + }, data() { return { title: '', @@ -32,18 +35,23 @@ export default { }; }, computed: { + ...mapGetters(['isSwimlanesOn']), disabled() { if (this.groupId) { return this.title === '' || !this.selectedProject.name; } return this.title === ''; }, + shouldDisplaySwimlanes() { + return this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn; + }, }, mounted() { this.$refs.input.focus(); eventHub.$on('setSelectedProject', this.setSelectedProject); }, methods: { + ...mapActions(['addListIssue', 'addListIssueFailure']), submit(e) { e.preventDefault(); if (this.title.trim() === '') return Promise.resolve(); @@ -70,21 +78,31 @@ export default { eventHub.$emit(`scroll-board-list-${this.list.id}`); this.cancel(); + if (this.shouldDisplaySwimlanes || this.glFeatures.graphqlBoardLists) { + this.addListIssue({ list: this.list, issue, position: 0 }); + } + return this.list .newIssue(issue) .then(() => { // Need this because our jQuery very kindly disables buttons on ALL form submissions $(this.$refs.submitButton).enable(); - boardsStore.setIssueDetail(issue); - boardsStore.setListDetail(this.list); + if (!this.shouldDisplaySwimlanes && !this.glFeatures.graphqlBoardLists) { + boardsStore.setIssueDetail(issue); + boardsStore.setListDetail(this.list); + } }) .catch(() => { // Need this because our jQuery very kindly disables buttons on ALL form submissions $(this.$refs.submitButton).enable(); // Remove the issue - this.list.removeIssue(issue); + if (this.shouldDisplaySwimlanes || this.glFeatures.graphqlBoardLists) { + this.addListIssueFailure({ list: this.list, issue }); + } else { + this.list.removeIssue(issue); + } // Show error message this.error = true; @@ -121,7 +139,7 @@ export default { <project-select v-if="groupId" :group-id="groupId" :list="list" /> <div class="clearfix gl-mt-3"> <gl-button - ref="submit-button" + ref="submitButton" :disabled="disabled" class="float-left" variant="success" @@ -129,9 +147,14 @@ export default { type="submit" >{{ __('Submit issue') }}</gl-button > - <gl-button class="float-right" type="button" variant="default" @click="cancel">{{ - __('Cancel') - }}</gl-button> + <gl-button + ref="cancelButton" + class="float-right" + type="button" + variant="default" + @click="cancel" + >{{ __('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 3149762ecdf..e2600883e89 100644 --- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue @@ -1,11 +1,12 @@ <script> import { GlDrawer, GlLabel } from '@gitlab/ui'; -import { mapActions, mapState } from 'vuex'; +import { mapActions, mapState, mapGetters } from 'vuex'; import { __ } from '~/locale'; import boardsStore from '~/boards/stores/boards_store'; import eventHub from '~/sidebar/event_hub'; import { isScopedLabel } from '~/lib/utils/common_utils'; -import { inactiveId } from '~/boards/constants'; +import { LIST } from '~/boards/constants'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; // NOTE: need to revisit how we handle headerHeight, because we have so many different header and footer options. export default { @@ -23,18 +24,20 @@ export default { BoardSettingsListTypes: () => import('ee_component/boards/components/board_settings_list_types.vue'), }, + mixins: [glFeatureFlagMixin()], computed: { - ...mapState(['activeId']), + ...mapGetters(['isSidebarOpen']), + ...mapState(['activeId', 'sidebarType', 'boardLists']), activeList() { /* Warning: Though a computed property it is not reactive because we are referencing a List Model class. Reactivity only applies to plain JS objects */ + if (this.glFeatures.graphqlBoardLists) { + return this.boardLists.find(({ id }) => id === this.activeId); + } return boardsStore.state.lists.find(({ id }) => id === this.activeId); }, - isSidebarOpen() { - return this.activeId !== inactiveId; - }, activeListLabel() { return this.activeList.label; }, @@ -44,18 +47,18 @@ export default { listTypeTitle() { return this.$options.labelListText; }, + showSidebar() { + return this.sidebarType === LIST; + }, }, created() { - eventHub.$on('sidebar.closeAll', this.closeSidebar); + eventHub.$on('sidebar.closeAll', this.unsetActiveId); }, beforeDestroy() { - eventHub.$off('sidebar.closeAll', this.closeSidebar); + eventHub.$off('sidebar.closeAll', this.unsetActiveId); }, methods: { - ...mapActions(['setActiveId']), - closeSidebar() { - this.setActiveId(inactiveId); - }, + ...mapActions(['unsetActiveId']), showScopedLabels(label) { return boardsStore.scopedLabels.enabled && isScopedLabel(label); }, @@ -65,10 +68,11 @@ export default { <template> <gl-drawer + v-if="showSidebar" class="js-board-settings-sidebar" :open="isSidebarOpen" :header-height="$options.headerHeight" - @close="closeSidebar" + @close="unsetActiveId" > <template #header>{{ $options.listSettingsText }}</template> <template v-if="isSidebarOpen"> diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index 3790c494085..d26f15c1723 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -83,7 +83,7 @@ export default Vue.extend({ $('.js-issue-board-sidebar', this.$el).each((i, el) => { $(el) - .data('glDropdown') + .data('deprecatedJQueryDropdown') .clearMenu(); }); } @@ -95,7 +95,7 @@ export default Vue.extend({ }, }, created() { - // Get events from glDropdown + // Get events from deprecatedJQueryDropdown eventHub.$on('sidebar.removeAssignee', this.removeAssignee); eventHub.$on('sidebar.addAssignee', this.addAssignee); eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees); diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue index 48f6ba6cfc7..271e1fc4b5f 100644 --- a/app/assets/javascripts/boards/components/boards_selector.vue +++ b/app/assets/javascripts/boards/components/boards_selector.vue @@ -36,10 +36,6 @@ export default { type: Object, required: true, }, - milestonePath: { - type: String, - required: true, - }, throttleDuration: { type: Number, default: 200, @@ -65,6 +61,10 @@ export default { type: String, required: true, }, + labelsWebUrl: { + type: String, + required: true, + }, projectId: { type: Number, required: true, @@ -335,8 +335,8 @@ export default { <board-form v-if="currentPage" - :milestone-path="milestonePath" :labels-path="labelsPath" + :labels-web-url="labelsWebUrl" :project-id="projectId" :group-id="groupId" :can-admin-board="canAdminBoard" diff --git a/app/assets/javascripts/boards/components/issuable_title.vue b/app/assets/javascripts/boards/components/issuable_title.vue new file mode 100644 index 00000000000..40627a9fab8 --- /dev/null +++ b/app/assets/javascripts/boards/components/issuable_title.vue @@ -0,0 +1,21 @@ +<script> +export default { + props: { + title: { + type: String, + required: true, + }, + refPath: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <div data-testid="issue-title"> + <p class="gl-font-weight-bold">{{ title }}</p> + <p class="gl-mb-0">{{ refPath }}</p> + </div> +</template> diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index d90928f35b6..8658f51e5cf 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -1,10 +1,9 @@ <script> import { sortBy } from 'lodash'; import { mapState } from 'vuex'; -import { GlLabel, GlTooltipDirective } from '@gitlab/ui'; +import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner'; import { sprintf, __ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; 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'; @@ -15,7 +14,7 @@ import { isScopedLabel } from '~/lib/utils/common_utils'; export default { components: { GlLabel, - Icon, + GlIcon, UserAvatarLink, TooltipOnTruncate, IssueDueDate, @@ -31,28 +30,23 @@ export default { type: Object, required: true, }, - issueLinkBase: { - type: String, - required: true, - }, list: { type: Object, required: false, default: () => ({}), }, - rootPath: { - type: String, - required: true, - }, updateFilters: { type: Boolean, required: false, default: false, }, + }, + inject: { groupId: { type: Number, - required: false, - default: null, + }, + rootPath: { + type: String, }, }, data() { @@ -148,7 +142,7 @@ export default { <div> <div class="d-flex board-card-header" dir="auto"> <h4 class="board-card-title gl-mb-0 gl-mt-0"> - <icon + <gl-icon v-if="issue.blocked" v-gl-tooltip name="issue-block" @@ -156,7 +150,7 @@ export default { class="issue-blocked-icon gl-mr-2" :aria-label="__('Blocked issue')" /> - <icon + <gl-icon v-if="issue.confidential" v-gl-tooltip name="eye-slash" diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue index 4add5ee646a..fb45de6e14d 100644 --- a/app/assets/javascripts/boards/components/issue_due_date.vue +++ b/app/assets/javascripts/boards/components/issue_due_date.vue @@ -1,7 +1,6 @@ <script> import dateFormat from 'dateformat'; -import { GlTooltip } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlTooltip, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import { getDayDifference, @@ -12,7 +11,7 @@ import { export default { components: { - Icon, + GlIcon, GlTooltip, }, props: { @@ -87,7 +86,7 @@ export default { <template> <span> <span ref="issueDueDate" :class="cssClass" class="board-card-info card-number"> - <icon :class="{ 'text-danger': isPastDue }" class="board-card-info-icon" name="calendar" /> + <gl-icon :class="{ 'text-danger': isPastDue }" class="board-card-info-icon" name="calendar" /> <time :class="{ 'text-danger': isPastDue }" datetime="date" class="board-card-info-text">{{ body }}</time> diff --git a/app/assets/javascripts/boards/components/issue_time_estimate.vue b/app/assets/javascripts/boards/components/issue_time_estimate.vue index e8b7689da13..fe56833016e 100644 --- a/app/assets/javascripts/boards/components/issue_time_estimate.vue +++ b/app/assets/javascripts/boards/components/issue_time_estimate.vue @@ -1,12 +1,11 @@ <script> -import { GlTooltip } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlTooltip, GlIcon } from '@gitlab/ui'; import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility'; import boardsStore from '../stores/boards_store'; export default { components: { - Icon, + GlIcon, GlTooltip, }, props: { @@ -34,7 +33,7 @@ export default { <template> <span> <span ref="issueTimeEstimate" class="board-card-info card-number"> - <icon name="hourglass" class="board-card-info-icon" /><time class="board-card-info-text">{{ + <gl-icon name="hourglass" class="board-card-info-icon" /><time class="board-card-info-text">{{ timeEstimate }}</time> </span> diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue index 66f59009714..cd4512f320f 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.vue +++ b/app/assets/javascripts/boards/components/modal/empty_state.vue @@ -1,9 +1,14 @@ <script> +/* eslint-disable vue/no-v-html */ +import { GlButton } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import ModalStore from '../../stores/modal_store'; import modalMixin from '../../mixins/modal_mixins'; export default { + components: { + GlButton, + }, mixins: [modalMixin], props: { newIssuePath: { @@ -53,17 +58,22 @@ export default { <div class="text-content"> <h4>{{ contents.title }}</h4> <p v-html="contents.content"></p> - <a v-if="activeTab === 'all'" :href="newIssuePath" class="btn btn-success btn-inverted">{{ - __('New issue') - }}</a> - <button + <gl-button + v-if="activeTab === 'all'" + :href="newIssuePath" + category="secondary" + variant="success" + > + {{ __('New issue') }} + </gl-button> + <gl-button v-if="activeTab === 'selected'" - class="btn btn-default" - type="button" + category="primary" + variant="default" @click="changeTab('all')" > {{ __('Open issues') }} - </button> + </gl-button> </div> </div> </div> diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue index c4953dda793..d28a03da97f 100644 --- a/app/assets/javascripts/boards/components/modal/footer.vue +++ b/app/assets/javascripts/boards/components/modal/footer.vue @@ -1,4 +1,5 @@ <script> +import { GlButton } from '@gitlab/ui'; import footerEEMixin from 'ee_else_ce/boards/mixins/modal_footer'; import { deprecatedCreateFlash as Flash } from '../../../flash'; import { __, n__ } from '../../../locale'; @@ -10,6 +11,7 @@ import boardsStore from '../../stores/boards_store'; export default { components: { ListsDropdown, + GlButton, }, mixins: [modalMixin, footerEEMixin], data() { @@ -65,14 +67,14 @@ export default { <template> <footer class="form-actions add-issues-footer"> <div class="float-left"> - <button :disabled="submitDisabled" class="btn btn-success" type="button" @click="addIssues"> + <gl-button :disabled="submitDisabled" category="primary" variant="success" @click="addIssues"> {{ submitText }} - </button> + </gl-button> <span class="inline add-issues-footer-to-list">{{ __('to list') }}</span> <lists-dropdown /> </div> - <button class="btn btn-default float-right" type="button" @click="toggleModal(false)"> + <gl-button class="float-right" @click="toggleModal(false)"> {{ __('Cancel') }} - </button> + </gl-button> </footer> </template> diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue index 573284d2b44..3e96ecca24c 100644 --- a/app/assets/javascripts/boards/components/modal/header.vue +++ b/app/assets/javascripts/boards/components/modal/header.vue @@ -1,5 +1,6 @@ <script> /* eslint-disable @gitlab/vue-require-i18n-strings */ +import { GlButton } from '@gitlab/ui'; import { __ } from '~/locale'; import ModalFilters from './filters'; import ModalTabs from './tabs.vue'; @@ -10,6 +11,7 @@ export default { components: { ModalTabs, ModalFilters, + GlButton, }, mixins: [modalMixin], props: { @@ -17,10 +19,6 @@ export default { type: Number, required: true, }, - milestonePath: { - type: String, - required: true, - }, labelPath: { type: String, required: true, @@ -43,7 +41,7 @@ export default { }, methods: { toggleAll() { - this.$refs.selectAllBtn.blur(); + this.$refs.selectAllBtn.$el.blur(); ModalStore.toggleAll(); }, @@ -55,28 +53,28 @@ export default { <header class="add-issues-header border-top-0 form-actions"> <h2 class="m-0"> Add issues - <button - type="button" + <gl-button + category="tertiary" + icon="close" class="close" data-dismiss="modal" :aria-label="__('Close')" @click="toggleModal(false)" - > - <span aria-hidden="true">×</span> - </button> + /> </h2> </header> <modal-tabs v-if="!loading && issuesCount > 0" /> <div v-if="showSearch" class="d-flex gl-mb-3"> <modal-filters :store="filter" /> - <button + <gl-button ref="selectAllBtn" - type="button" - class="btn btn-success btn-inverted gl-ml-3" + category="secondary" + variant="success" + class="gl-ml-3" @click="toggleAll" > {{ selectAllText }} - </button> + </gl-button> </div> </div> </template> diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue index 20344b66140..817b3bdddb0 100644 --- a/app/assets/javascripts/boards/components/modal/index.vue +++ b/app/assets/javascripts/boards/components/modal/index.vue @@ -26,22 +26,10 @@ export default { type: String, required: true, }, - issueLinkBase: { - type: String, - required: true, - }, - rootPath: { - type: String, - required: true, - }, projectId: { type: Number, required: true, }, - milestonePath: { - type: String, - required: true, - }, labelPath: { type: String, required: true, @@ -149,17 +137,8 @@ export default { class="add-issues-modal d-flex position-fixed position-top-0 position-bottom-0 position-left-0 position-right-0 h-100" > <div class="add-issues-container d-flex flex-column m-auto rounded"> - <modal-header - :project-id="projectId" - :milestone-path="milestonePath" - :label-path="labelPath" - /> - <modal-list - v-if="!loading && showList && !filterLoading" - :issue-link-base="issueLinkBase" - :root-path="rootPath" - :empty-state-svg="emptyStateSvg" - /> + <modal-header :project-id="projectId" :label-path="labelPath" /> + <modal-list v-if="!loading && showList && !filterLoading" :empty-state-svg="emptyStateSvg" /> <empty-state v-if="showEmptyState" :new-issue-path="newIssuePath" diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue index 78e3351a79e..219263bd9b9 100644 --- a/app/assets/javascripts/boards/components/modal/list.vue +++ b/app/assets/javascripts/boards/components/modal/list.vue @@ -1,23 +1,15 @@ <script> import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import ModalStore from '../../stores/modal_store'; import IssueCardInner from '../issue_card_inner.vue'; export default { components: { IssueCardInner, - Icon, + GlIcon, }, props: { - issueLinkBase: { - type: String, - required: true, - }, - rootPath: { - type: String, - required: true, - }, emptyStateSvg: { type: String, required: true, @@ -134,8 +126,8 @@ export default { class="board-card position-relative p-3 rounded" @click="toggleIssue($event, issue)" > - <issue-card-inner :issue="issue" :issue-link-base="issueLinkBase" :root-path="rootPath" /> - <icon + <issue-card-inner :issue="issue" /> + <gl-icon v-if="issue.selected" :aria-label="'Issue #' + issue.id + ' selected'" name="mobile-issue-close" diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue index 3fbe8fe1be7..fe10e7fb856 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue @@ -1,13 +1,12 @@ <script> -import { GlLink } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlLink, GlIcon } from '@gitlab/ui'; import ModalStore from '../../stores/modal_store'; import boardsStore from '../../stores/boards_store'; export default { components: { GlLink, - Icon, + GlIcon, }, data() { return { @@ -29,7 +28,7 @@ export default { <div class="dropdown inline"> <button class="dropdown-menu-toggle" type="button" data-toggle="dropdown" aria-expanded="false"> <span :style="{ backgroundColor: selected.label.color }" class="dropdown-label-box"> </span> - {{ selected.title }} <icon name="chevron-down" /> + {{ selected.title }} <gl-icon name="chevron-down" class="dropdown-menu-toggle-icon" /> </button> <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"> <ul> diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index 2b9fdf11b37..2e356f1353a 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -6,6 +6,7 @@ import axios from '~/lib/utils/axios_utils'; import { deprecatedCreateFlash as flash } from '~/flash'; import CreateLabelDropdown from '../../create_label'; import boardsStore from '../stores/boards_store'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; $(document) .off('created.label') @@ -36,7 +37,7 @@ export default function initNewListDropdown() { $dropdownToggle.data('projectPath'), ); - $dropdownToggle.glDropdown({ + initDeprecatedJQueryDropdown($dropdownToggle, { data(term, callback) { axios .get($dropdownToggle.attr('data-list-labels-path')) diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue index 598e92726c1..59e7620962a 100644 --- a/app/assets/javascripts/boards/components/project_select.vue +++ b/app/assets/javascripts/boards/components/project_select.vue @@ -1,30 +1,30 @@ <script> import $ from 'jquery'; import { escape } from 'lodash'; -import { GlLoadingIcon } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import eventHub from '../eventhub'; import Api from '../../api'; import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default { name: 'BoardProjectSelect', components: { - Icon, + GlIcon, GlLoadingIcon, }, props: { - groupId: { - type: Number, - required: true, - default: 0, - }, list: { type: Object, required: true, }, }, + inject: { + groupId: { + type: Number, + }, + }, data() { return { loading: true, @@ -37,7 +37,7 @@ export default { }, }, mounted() { - $(this.$refs.projectsDropdown).glDropdown({ + initDeprecatedJQueryDropdown($(this.$refs.projectsDropdown), { filterable: true, filterRemote: true, search: { @@ -105,13 +105,13 @@ export default { data-toggle="dropdown" aria-expanded="false" > - {{ selectedProjectName }} <icon name="chevron-down" /> + {{ 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')" /> - <icon name="search" class="dropdown-input-search" data-hidden="true" /> + <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> diff --git a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue new file mode 100644 index 00000000000..8df03ea581f --- /dev/null +++ b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue @@ -0,0 +1,79 @@ +<script> +import { GlButton, GlLoadingIcon } from '@gitlab/ui'; + +export default { + components: { GlButton, GlLoadingIcon }, + props: { + title: { + type: String, + required: false, + default: '', + }, + loading: { + type: Boolean, + required: false, + default: false, + }, + }, + inject: ['canUpdate'], + data() { + return { + edit: false, + }; + }, + destroyed() { + window.removeEventListener('click', this.collapseWhenOffClick); + }, + methods: { + collapseWhenOffClick({ target }) { + if (!this.$el.contains(target)) { + this.collapse(); + } + }, + expand() { + if (this.edit) { + return; + } + + this.edit = true; + this.$emit('changed', this.edit); + window.addEventListener('click', this.collapseWhenOffClick); + }, + collapse() { + if (!this.edit) { + return; + } + + this.edit = false; + this.$emit('changed', this.edit); + window.removeEventListener('click', this.collapseWhenOffClick); + }, + }, +}; +</script> + +<template> + <div> + <div class="gl-display-flex gl-justify-content-space-between gl-mb-3"> + <span class="gl-vertical-align-middle"> + <span data-testid="title">{{ title }}</span> + <gl-loading-icon v-if="loading" inline class="gl-ml-2" /> + </span> + <gl-button + v-if="canUpdate" + variant="link" + class="gl-text-gray-900!" + data-testid="edit-button" + @click="expand()" + > + {{ __('Edit') }} + </gl-button> + </div> + <div v-show="!edit" class="gl-text-gray-400" data-testid="collapsed-content"> + <slot name="collapsed">{{ __('None') }}</slot> + </div> + <div v-show="edit" data-testid="expanded-content"> + <slot></slot> + </div> + </div> +</template> diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js index 35c52558cac..2f64014a949 100644 --- a/app/assets/javascripts/boards/constants.js +++ b/app/assets/javascripts/boards/constants.js @@ -15,6 +15,9 @@ export const ListType = { export const inactiveId = 0; +export const ISSUABLE = 'issuable'; +export const LIST = 'list'; + export default { BoardType, ListType, diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index b7966dd869d..fff89832bf0 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -27,6 +27,11 @@ export default class FilteredSearchBoards extends FilteredSearchManager { updateObject(path) { this.store.path = path.substr(1); + if (gon.features.boardsWithSwimlanes || gon.features.graphqlBoardLists) { + boardsStore.updateFiltersUrl(); + boardsStore.performSearch(); + } + if (this.updateUrl) { boardsStore.updateFiltersUrl(); } diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 971edd71eec..1173c6d0578 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -1,6 +1,6 @@ import $ from 'jquery'; import Vue from 'vue'; -import { mapActions } from 'vuex'; +import { mapActions, mapState } from 'vuex'; import 'ee_else_ce/boards/models/issue'; import 'ee_else_ce/boards/models/list'; @@ -24,7 +24,6 @@ import { deprecatedCreateFlash as Flash } from '~/flash'; import { __ } from '~/locale'; import './models/label'; import './models/assignee'; -import { BoardType } from './constants'; import toggleFocusMode from '~/boards/toggle_focus'; import FilteredSearchBoards from '~/boards/filtered_search_boards'; @@ -42,11 +41,9 @@ import { NavigationType, convertObjectPropsToCamelCase, parseBoolean, + urlParamsToObject, } from '~/lib/utils/common_utils'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher'; -import projectBoardQuery from './queries/project_board.query.graphql'; -import groupQuery from './queries/group_board.query.graphql'; Vue.use(VueApollo); @@ -85,6 +82,11 @@ export default () => { BoardAddIssuesModal, BoardSettingsSidebar: () => import('~/boards/components/board_settings_sidebar.vue'), }, + provide: { + boardId: $boardApp.dataset.boardId, + groupId: Number($boardApp.dataset.groupId) || null, + rootPath: $boardApp.dataset.rootPath, + }, store, apolloProvider, data() { @@ -94,16 +96,14 @@ export default () => { boardsEndpoint: $boardApp.dataset.boardsEndpoint, recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint, listsEndpoint: $boardApp.dataset.listsEndpoint, - boardId: $boardApp.dataset.boardId, disabled: parseBoolean($boardApp.dataset.disabled), - issueLinkBase: $boardApp.dataset.issueLinkBase, - rootPath: $boardApp.dataset.rootPath, bulkUpdatePath: $boardApp.dataset.bulkUpdatePath, detailIssue: boardsStore.detail, parent: $boardApp.dataset.parent, }; }, computed: { + ...mapState(['isShowingEpicsSwimlanes']), detailIssueVisible() { return Object.keys(this.detailIssue.issue).length; }, @@ -114,10 +114,15 @@ export default () => { recentBoardsEndpoint: this.recentBoardsEndpoint, listsEndpoint: this.listsEndpoint, bulkUpdatePath: this.bulkUpdatePath, - boardId: this.boardId, + boardId: $boardApp.dataset.boardId, fullPath: $boardApp.dataset.fullPath, }; - this.setInitialBoardData({ ...endpoints, boardType: this.parent }); + this.setInitialBoardData({ + ...endpoints, + boardType: this.parent, + disabled: this.disabled, + showPromotion: parseBoolean($boardApp.getAttribute('data-show-promotion')), + }); boardsStore.setEndpoints(endpoints); boardsStore.rootPath = this.boardsEndpoint; @@ -125,55 +130,24 @@ export default () => { eventHub.$on('newDetailIssue', this.updateDetailIssue); eventHub.$on('clearDetailIssue', this.clearDetailIssue); sidebarEventHub.$on('toggleSubscription', this.toggleSubscription); + eventHub.$on('performSearch', this.performSearch); }, beforeDestroy() { eventHub.$off('updateTokens', this.updateTokens); eventHub.$off('newDetailIssue', this.updateDetailIssue); eventHub.$off('clearDetailIssue', this.clearDetailIssue); sidebarEventHub.$off('toggleSubscription', this.toggleSubscription); + eventHub.$off('performSearch', this.performSearch); }, mounted() { this.filterManager = new FilteredSearchBoards(boardsStore.filter, true, boardsStore.cantEdit); this.filterManager.setup(); - boardsStore.disabled = this.disabled; - - if (gon.features.graphqlBoardLists) { - this.$apollo.addSmartQuery('lists', { - query() { - return this.parent === BoardType.group ? groupQuery : projectBoardQuery; - }, - variables() { - return { - fullPath: this.state.endpoints.fullPath, - boardId: `gid://gitlab/Board/${this.boardId}`, - }; - }, - update(data) { - return this.getNodes(data); - }, - result({ data, error }) { - if (error) { - throw error; - } - - const lists = this.getNodes(data); + this.performSearch(); - lists.forEach(list => - boardsStore.addList({ - ...list, - id: getIdFromGraphQLId(list.id), - }), - ); + boardsStore.disabled = this.disabled; - boardsStore.addBlankState(); - setPromotionState(boardsStore); - }, - error() { - Flash(__('An error occurred while fetching the board lists. Please try again.')); - }, - }); - } else { + if (!gon.features.graphqlBoardLists) { boardsStore .all() .then(res => res.data) @@ -189,10 +163,22 @@ export default () => { } }, methods: { - ...mapActions(['setInitialBoardData']), + ...mapActions([ + 'setInitialBoardData', + 'setFilters', + 'fetchEpicsSwimlanes', + 'fetchIssuesForAllLists', + ]), updateTokens() { this.filterManager.updateTokens(); }, + performSearch() { + this.setFilters(convertObjectPropsToCamelCase(urlParamsToObject(window.location.search))); + if (gon.features.boardsWithSwimlanes && this.isShowingEpicsSwimlanes) { + this.fetchEpicsSwimlanes(false); + this.fetchIssuesForAllLists(); + } + }, updateDetailIssue(newIssue, multiSelect = false) { const { sidebarInfoEndpoint } = newIssue; if (sidebarInfoEndpoint && newIssue.subscribed === undefined) { @@ -354,6 +340,8 @@ export default () => { class="btn btn-success gl-ml-3" type="button" data-placement="bottom" + data-track-event="click_button" + data-track-label="board_add_issues" ref="addIssuesButton" :class="{ 'disabled': disabled }" :title="tooltipTitle" diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index 98eac35b2ed..822e6d62ab3 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -15,7 +15,7 @@ class ListIssue { this.labels = []; this.assignees = []; this.selected = false; - this.position = obj.position || obj.relative_position || Infinity; + this.position = obj.position || obj.relative_position || obj.relativePosition || Infinity; this.isFetching = { subscriptions: true, }; diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index b8b30c958a9..2f6caffbf84 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -47,7 +47,7 @@ class List { this.loading = true; this.loadingMore = false; this.issues = obj.issues || []; - this.issuesSize = obj.issuesSize ? obj.issuesSize : 0; + this.issuesSize = obj.issuesSize || obj.issuesCount || 0; this.maxIssueCount = obj.maxIssueCount || obj.max_issue_count || 0; if (obj.label) { diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js index 73d37459bfe..51bb72b7657 100644 --- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js +++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js @@ -27,7 +27,7 @@ export default () => { hasMissingBoards: parseBoolean(dataset.hasMissingBoards), canAdminBoard: parseBoolean(dataset.canAdminBoard), multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable), - projectId: Number(dataset.projectId), + projectId: dataset.projectId ? Number(dataset.projectId) : 0, groupId: Number(dataset.groupId), scopedIssueBoardFeatureEnabled: parseBoolean(dataset.scopedIssueBoardFeatureEnabled), weights: JSON.parse(dataset.weights), diff --git a/app/assets/javascripts/boards/queries/board_list_create.mutation.graphql b/app/assets/javascripts/boards/queries/board_list_create.mutation.graphql new file mode 100644 index 00000000000..dcfe69222a0 --- /dev/null +++ b/app/assets/javascripts/boards/queries/board_list_create.mutation.graphql @@ -0,0 +1,10 @@ +#import "./board_list.fragment.graphql" + +mutation CreateBoardList($boardId: BoardID!, $backlog: Boolean) { + boardListCreate(input: { boardId: $boardId, backlog: $backlog }) { + list { + ...BoardListFragment + } + errors + } +} diff --git a/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql b/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql index 8abd79332fb..d85b736720b 100644 --- a/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql +++ b/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql @@ -4,7 +4,7 @@ fragment BoardListShared on BoardList { position listType collapsed - maxIssueCount + issuesCount label { id title diff --git a/app/assets/javascripts/boards/queries/board_list_update.mutation.graphql b/app/assets/javascripts/boards/queries/board_list_update.mutation.graphql new file mode 100644 index 00000000000..b474c9acb93 --- /dev/null +++ b/app/assets/javascripts/boards/queries/board_list_update.mutation.graphql @@ -0,0 +1,10 @@ +#import "./board_list.fragment.graphql" + +mutation UpdateBoardList($listId: ID!, $position: Int, $collapsed: Boolean) { + updateBoardList(input: { listId: $listId, position: $position, collapsed: $collapsed }) { + list { + ...BoardListFragment + } + errors + } +} diff --git a/app/assets/javascripts/boards/queries/group_lists_issues.query.graphql b/app/assets/javascripts/boards/queries/group_lists_issues.query.graphql deleted file mode 100644 index 724c7884c58..00000000000 --- a/app/assets/javascripts/boards/queries/group_lists_issues.query.graphql +++ /dev/null @@ -1,18 +0,0 @@ -#import "./issue.fragment.graphql" - -query GroupListIssues($fullPath: ID!, $boardId: ID!) { - group(fullPath: $fullPath) { - board(id: $boardId) { - lists { - nodes { - id - issues { - nodes { - ...IssueNode - } - } - } - } - } - } -} diff --git a/app/assets/javascripts/boards/queries/issue.fragment.graphql b/app/assets/javascripts/boards/queries/issue.fragment.graphql index 89d56b895a4..4b429f875a6 100644 --- a/app/assets/javascripts/boards/queries/issue.fragment.graphql +++ b/app/assets/javascripts/boards/queries/issue.fragment.graphql @@ -7,14 +7,10 @@ fragment IssueNode on Issue { referencePath: reference(full: true) dueDate timeEstimate - weight confidential webUrl subscribed - blocked - epic { - id - } + relativePosition assignees { nodes { ...User diff --git a/app/assets/javascripts/boards/queries/issue_move_list.mutation.graphql b/app/assets/javascripts/boards/queries/issue_move_list.mutation.graphql new file mode 100644 index 00000000000..ff6aa597f48 --- /dev/null +++ b/app/assets/javascripts/boards/queries/issue_move_list.mutation.graphql @@ -0,0 +1,28 @@ +#import "ee_else_ce/boards/queries/issue.fragment.graphql" + +mutation IssueMoveList( + $projectPath: ID! + $iid: String! + $boardId: ID! + $fromListId: ID + $toListId: ID + $moveBeforeId: ID + $moveAfterId: ID +) { + issueMoveList( + input: { + projectPath: $projectPath + iid: $iid + boardId: $boardId + fromListId: $fromListId + toListId: $toListId + moveBeforeId: $moveBeforeId + moveAfterId: $moveAfterId + } + ) { + issue { + ...IssueNode + } + errors + } +} diff --git a/app/assets/javascripts/boards/queries/lists_issues.query.graphql b/app/assets/javascripts/boards/queries/lists_issues.query.graphql new file mode 100644 index 00000000000..c66cdf68cf4 --- /dev/null +++ b/app/assets/javascripts/boards/queries/lists_issues.query.graphql @@ -0,0 +1,39 @@ +#import "ee_else_ce/boards/queries/issue.fragment.graphql" + +query ListIssues( + $fullPath: ID! + $boardId: ID! + $id: ID + $filters: BoardIssueInput + $isGroup: Boolean = false + $isProject: Boolean = false +) { + group(fullPath: $fullPath) @include(if: $isGroup) { + board(id: $boardId) { + lists(id: $id) { + nodes { + id + issues(filters: $filters) { + nodes { + ...IssueNode + } + } + } + } + } + } + project(fullPath: $fullPath) @include(if: $isProject) { + board(id: $boardId) { + lists(id: $id) { + nodes { + id + issues(filters: $filters) { + nodes { + ...IssueNode + } + } + } + } + } + } +} diff --git a/app/assets/javascripts/boards/queries/project_lists_issues.query.graphql b/app/assets/javascripts/boards/queries/project_lists_issues.query.graphql deleted file mode 100644 index 149b76848ef..00000000000 --- a/app/assets/javascripts/boards/queries/project_lists_issues.query.graphql +++ /dev/null @@ -1,18 +0,0 @@ -#import "./issue.fragment.graphql" - -query ProjectListIssues($fullPath: ID!, $boardId: ID!) { - project(fullPath: $fullPath) { - board(id: $boardId) { - lists { - nodes { - id - issues { - nodes { - ...IssueNode - } - } - } - } - } - } -} diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index b4be7546252..4b81d9c73ef 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -1,66 +1,248 @@ -import * as types from './mutation_types'; +import Cookies from 'js-cookie'; +import { sortBy, pick } from 'lodash'; +import createFlash from '~/flash'; +import { __ } from '~/locale'; +import { parseBoolean } from '~/lib/utils/common_utils'; import createDefaultClient from '~/lib/graphql'; -import { BoardType } from '~/boards/constants'; -import { formatListIssues } from '../boards_util'; -import groupListsIssuesQuery from '../queries/group_lists_issues.query.graphql'; -import projectListsIssuesQuery from '../queries/project_lists_issues.query.graphql'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { BoardType, ListType, inactiveId } from '~/boards/constants'; +import * as types from './mutation_types'; +import { formatListIssues, fullBoardId } from '../boards_util'; +import boardStore from '~/boards/stores/boards_store'; -const gqlClient = createDefaultClient(); +import listsIssuesQuery from '../queries/lists_issues.query.graphql'; +import projectBoardQuery from '../queries/project_board.query.graphql'; +import groupBoardQuery from '../queries/group_board.query.graphql'; +import createBoardListMutation from '../queries/board_list_create.mutation.graphql'; +import updateBoardListMutation from '../queries/board_list_update.mutation.graphql'; +import issueMoveListMutation from '../queries/issue_move_list.mutation.graphql'; const notImplemented = () => { /* eslint-disable-next-line @gitlab/require-i18n-strings */ throw new Error('Not implemented!'); }; +export const gqlClient = createDefaultClient(); + export default { setInitialBoardData: ({ commit }, data) => { commit(types.SET_INITIAL_BOARD_DATA, data); }, - setActiveId({ commit }, id) { - commit(types.SET_ACTIVE_ID, id); + setActiveId({ commit }, { id, sidebarType }) { + commit(types.SET_ACTIVE_ID, { id, sidebarType }); }, - fetchLists: () => { - notImplemented(); + unsetActiveId({ dispatch }) { + dispatch('setActiveId', { id: inactiveId, sidebarType: '' }); }, + setFilters: ({ commit }, filters) => { + const filterParams = pick(filters, [ + 'assigneeUsername', + 'authorUsername', + 'labelName', + 'milestoneTitle', + 'releaseTag', + 'search', + ]); + commit(types.SET_FILTERS, filterParams); + }, + + fetchLists: ({ commit, state, dispatch }) => { + const { endpoints, boardType } = state; + const { fullPath, boardId } = endpoints; + + let query; + if (boardType === BoardType.group) { + query = groupBoardQuery; + } else if (boardType === BoardType.project) { + query = projectBoardQuery; + } else { + createFlash(__('Invalid board')); + return Promise.reject(); + } + + const variables = { + fullPath, + boardId: fullBoardId(boardId), + }; + + return gqlClient + .query({ + query, + variables, + }) + .then(({ data }) => { + let { lists } = data[boardType]?.board; + // Temporarily using positioning logic from boardStore + lists = lists.nodes.map(list => + boardStore.updateListPosition({ + ...list, + doNotFetchIssues: true, + }), + ); + commit(types.RECEIVE_BOARD_LISTS_SUCCESS, sortBy(lists, 'position')); + // Backlog list needs to be created if it doesn't exist + if (!lists.find(l => l.type === ListType.backlog)) { + dispatch('createList', { backlog: true }); + } + dispatch('showWelcomeList'); + }) + .catch(() => { + createFlash( + __('An error occurred while fetching the board lists. Please reload the page.'), + ); + }); + }, + + // This action only supports backlog list creation at this stage + // Future iterations will add the ability to create other list types + createList: ({ state, commit, dispatch }, { backlog = false }) => { + const { boardId } = state.endpoints; + gqlClient + .mutate({ + mutation: createBoardListMutation, + variables: { + boardId: fullBoardId(boardId), + backlog, + }, + }) + .then(({ data }) => { + if (data?.boardListCreate?.errors.length) { + commit(types.CREATE_LIST_FAILURE); + } else { + const list = data.boardListCreate?.list; + dispatch('addList', list); + } + }) + .catch(() => { + commit(types.CREATE_LIST_FAILURE); + }); + }, + + addList: ({ state, commit }, list) => { + const lists = state.boardLists; + // Temporarily using positioning logic from boardStore + lists.push(boardStore.updateListPosition({ ...list, doNotFetchIssues: true })); + commit(types.RECEIVE_BOARD_LISTS_SUCCESS, sortBy(lists, 'position')); + }, + + showWelcomeList: ({ state, dispatch }) => { + if (state.disabled) { + return; + } + if ( + state.boardLists.find(list => list.type !== ListType.backlog && list.type !== ListType.closed) + ) { + return; + } + if (parseBoolean(Cookies.get('issue_board_welcome_hidden'))) { + return; + } + + dispatch('addList', { + id: 'blank', + listType: ListType.blank, + title: __('Welcome to your issue board!'), + position: 0, + }); + }, + + showPromotionList: () => {}, + generateDefaultLists: () => { notImplemented(); }, - createList: () => { - notImplemented(); + moveList: ({ state, commit, dispatch }, { listId, newIndex, adjustmentValue }) => { + const { boardLists } = state; + const backupList = [...boardLists]; + const movedList = boardLists.find(({ id }) => id === listId); + + const newPosition = newIndex - 1; + const listAtNewIndex = boardLists[newIndex]; + + movedList.position = newPosition; + listAtNewIndex.position += adjustmentValue; + commit(types.MOVE_LIST, { + movedList, + listAtNewIndex, + }); + + dispatch('updateList', { listId, position: newPosition, backupList }); }, - updateList: () => { - notImplemented(); + updateList: ({ commit }, { listId, position, collapsed, backupList }) => { + gqlClient + .mutate({ + mutation: updateBoardListMutation, + variables: { + listId, + position, + collapsed, + }, + }) + .then(({ data }) => { + if (data?.updateBoardList?.errors.length) { + commit(types.UPDATE_LIST_FAILURE, backupList); + } + }) + .catch(() => { + commit(types.UPDATE_LIST_FAILURE, backupList); + }); }, deleteList: () => { notImplemented(); }, - fetchIssuesForList: () => { - notImplemented(); + fetchIssuesForList: ({ state, commit }, listId) => { + const { endpoints, boardType, filterParams } = state; + const { fullPath, boardId } = endpoints; + + const variables = { + fullPath, + boardId: fullBoardId(boardId), + id: listId, + filters: filterParams, + isGroup: boardType === BoardType.group, + isProject: boardType === BoardType.project, + }; + + return gqlClient + .query({ + query: listsIssuesQuery, + context: { + isSingleRequest: true, + }, + variables, + }) + .then(({ data }) => { + const { lists } = data[boardType]?.board; + const listIssues = formatListIssues(lists); + commit(types.RECEIVE_ISSUES_FOR_LIST_SUCCESS, { listIssues, listId }); + }) + .catch(() => commit(types.RECEIVE_ISSUES_FOR_LIST_FAILURE, listId)); }, fetchIssuesForAllLists: ({ state, commit }) => { commit(types.REQUEST_ISSUES_FOR_ALL_LISTS); - const { endpoints, boardType } = state; + const { endpoints, boardType, filterParams } = state; const { fullPath, boardId } = endpoints; - const query = boardType === BoardType.group ? groupListsIssuesQuery : projectListsIssuesQuery; - const variables = { fullPath, - boardId: `gid://gitlab/Board/${boardId}`, + boardId: fullBoardId(boardId), + filters: filterParams, + isGroup: boardType === BoardType.group, + isProject: boardType === BoardType.project, }; return gqlClient .query({ - query, + query: listsIssuesQuery, variables, }) .then(({ data }) => { @@ -71,14 +253,56 @@ export default { .catch(() => commit(types.RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE)); }, - moveIssue: () => { - notImplemented(); + moveIssue: ( + { state, commit }, + { issueId, issueIid, issuePath, fromListId, toListId, moveBeforeId, moveAfterId }, + ) => { + const originalIssue = state.issues[issueId]; + const fromList = state.issuesByListId[fromListId]; + const originalIndex = fromList.indexOf(Number(issueId)); + commit(types.MOVE_ISSUE, { originalIssue, fromListId, toListId, moveBeforeId, moveAfterId }); + + const { boardId } = state.endpoints; + const [fullProjectPath] = issuePath.split(/[#]/); + + gqlClient + .mutate({ + mutation: issueMoveListMutation, + variables: { + projectPath: fullProjectPath, + boardId: fullBoardId(boardId), + iid: issueIid, + fromListId: getIdFromGraphQLId(fromListId), + toListId: getIdFromGraphQLId(toListId), + moveBeforeId, + moveAfterId, + }, + }) + .then(({ data }) => { + if (data?.issueMoveList?.errors.length) { + commit(types.MOVE_ISSUE_FAILURE, { originalIssue, fromListId, toListId, originalIndex }); + } else { + const issue = data.issueMoveList?.issue; + commit(types.MOVE_ISSUE_SUCCESS, { issue }); + } + }) + .catch(() => + commit(types.MOVE_ISSUE_FAILURE, { originalIssue, fromListId, toListId, originalIndex }), + ); }, createNewIssue: () => { notImplemented(); }, + addListIssue: ({ commit }, { list, issue, position }) => { + commit(types.ADD_ISSUE_TO_LIST, { list, issue, position }); + }, + + addListIssueFailure: ({ commit }, { list, issue }) => { + commit(types.ADD_ISSUE_TO_LIST_FAILURE, { list, issue }); + }, + fetchBacklog: () => { notImplemented(); }, diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 30c71d64085..faf4f9ebfd3 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -15,6 +15,7 @@ import { import { __ } from '~/locale'; import axios from '~/lib/utils/axios_utils'; import { mergeUrlParams } from '~/lib/utils/url_utility'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import eventHub from '../eventhub'; import { ListType } from '../constants'; import IssueProject from '../models/project'; @@ -303,7 +304,11 @@ const boardsStore = { onNewListIssueResponse(list, issue, data) { issue.refreshData(data); - if (list.issuesSize > 1) { + if ( + !gon.features.boardsWithSwimlanes && + !gon.features.graphqlBoardLists && + list.issues.length > 1 + ) { const moveBeforeId = list.issues[1].id; this.moveIssue(issue.id, null, null, null, moveBeforeId); } @@ -513,6 +518,10 @@ const boardsStore = { eventHub.$emit('updateTokens'); }, + performSearch() { + eventHub.$emit('performSearch'); + }, + setListDetail(newList) { this.detail.list = newList; }, @@ -706,6 +715,10 @@ const boardsStore = { }, newIssue(id, issue) { + if (typeof id === 'string') { + id = getIdFromGraphQLId(id); + } + return axios.post(this.generateIssuesPath(id), { issue, }); @@ -714,6 +727,10 @@ const boardsStore = { newListIssue(list, issue) { list.addIssue(issue, null, 0); list.issuesSize += 1; + let listId = list.id; + if (typeof listId === 'string') { + listId = getIdFromGraphQLId(listId); + } return this.newIssue(list.id, issue) .then(res => res.data) @@ -854,21 +871,6 @@ const boardsStore = { }, refreshIssueData(issue, obj) { - // issue.id = obj.id; - // issue.iid = obj.iid; - // issue.title = obj.title; - // issue.confidential = obj.confidential; - // issue.dueDate = obj.due_date || obj.dueDate; - // issue.sidebarInfoEndpoint = obj.issue_sidebar_endpoint; - // issue.referencePath = obj.reference_path || obj.referencePath; - // issue.path = obj.real_path || obj.webUrl; - // issue.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint; - // issue.project_id = obj.project_id; - // issue.timeEstimate = obj.time_estimate || obj.timeEstimate; - // issue.assignableLabelsEndpoint = obj.assignable_labels_endpoint; - // issue.blocked = obj.blocked; - // issue.epic = obj.epic; - const convertedObj = convertObjectPropsToCamelCase(obj, { dropKeys: ['issue_sidebar_endpoint', 'real_path', 'webUrl'], }); diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js index 4de1576099d..3688476dc5f 100644 --- a/app/assets/javascripts/boards/stores/getters.js +++ b/app/assets/javascripts/boards/stores/getters.js @@ -1,3 +1,25 @@ +import { inactiveId } from '../constants'; + export default { getLabelToggleState: state => (state.isShowingLabels ? 'on' : 'off'), + isSidebarOpen: state => state.activeId !== inactiveId, + isSwimlanesOn: state => { + if (!gon?.features?.boardsWithSwimlanes) { + return false; + } + + return state.isShowingEpicsSwimlanes; + }, + getIssueById: state => id => { + return state.issues[id] || {}; + }, + + getIssues: (state, getters) => listId => { + const listIssueIds = state.issuesByListId[listId] || []; + return listIssueIds.map(id => getters.getIssueById(id)); + }, + + getActiveIssue: state => { + return state.issues[state.activeId] || {}; + }, }; diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js index 0f96dc2e287..f0a283f6161 100644 --- a/app/assets/javascripts/boards/stores/mutation_types.js +++ b/app/assets/javascripts/boards/stores/mutation_types.js @@ -1,25 +1,34 @@ export const SET_INITIAL_BOARD_DATA = 'SET_INITIAL_BOARD_DATA'; +export const SET_FILTERS = 'SET_FILTERS'; +export const CREATE_LIST_SUCCESS = 'CREATE_LIST_SUCCESS'; +export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE'; +export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS'; +export const SHOW_PROMOTION_LIST = 'SHOW_PROMOTION_LIST'; export const REQUEST_ADD_LIST = 'REQUEST_ADD_LIST'; export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS'; export const RECEIVE_ADD_LIST_ERROR = 'RECEIVE_ADD_LIST_ERROR'; -export const REQUEST_UPDATE_LIST = 'REQUEST_UPDATE_LIST'; -export const RECEIVE_UPDATE_LIST_SUCCESS = 'RECEIVE_UPDATE_LIST_SUCCESS'; -export const RECEIVE_UPDATE_LIST_ERROR = 'RECEIVE_UPDATE_LIST_ERROR'; +export const MOVE_LIST = 'MOVE_LIST'; +export const UPDATE_LIST_FAILURE = 'UPDATE_LIST_FAILURE'; export const REQUEST_REMOVE_LIST = 'REQUEST_REMOVE_LIST'; export const RECEIVE_REMOVE_LIST_SUCCESS = 'RECEIVE_REMOVE_LIST_SUCCESS'; export const RECEIVE_REMOVE_LIST_ERROR = 'RECEIVE_REMOVE_LIST_ERROR'; export const REQUEST_ISSUES_FOR_ALL_LISTS = 'REQUEST_ISSUES_FOR_ALL_LISTS'; +export const RECEIVE_ISSUES_FOR_LIST_FAILURE = 'RECEIVE_ISSUES_FOR_LIST_FAILURE'; +export const RECEIVE_ISSUES_FOR_LIST_SUCCESS = 'RECEIVE_ISSUES_FOR_LIST_SUCCESS'; export const RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS = 'RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS'; export const RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE = 'RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE'; export const REQUEST_ADD_ISSUE = 'REQUEST_ADD_ISSUE'; export const RECEIVE_ADD_ISSUE_SUCCESS = 'RECEIVE_ADD_ISSUE_SUCCESS'; export const RECEIVE_ADD_ISSUE_ERROR = 'RECEIVE_ADD_ISSUE_ERROR'; -export const REQUEST_MOVE_ISSUE = 'REQUEST_MOVE_ISSUE'; -export const RECEIVE_MOVE_ISSUE_SUCCESS = 'RECEIVE_MOVE_ISSUE_SUCCESS'; -export const RECEIVE_MOVE_ISSUE_ERROR = 'RECEIVE_MOVE_ISSUE_ERROR'; +export const MOVE_ISSUE = 'MOVE_ISSUE'; +export const MOVE_ISSUE_SUCCESS = 'MOVE_ISSUE_SUCCESS'; +export const MOVE_ISSUE_FAILURE = 'MOVE_ISSUE_FAILURE'; export const REQUEST_UPDATE_ISSUE = 'REQUEST_UPDATE_ISSUE'; export const RECEIVE_UPDATE_ISSUE_SUCCESS = 'RECEIVE_UPDATE_ISSUE_SUCCESS'; export const RECEIVE_UPDATE_ISSUE_ERROR = 'RECEIVE_UPDATE_ISSUE_ERROR'; +export const ADD_ISSUE_TO_LIST = 'ADD_ISSUE_TO_LIST'; +export const ADD_ISSUE_TO_LIST_FAILURE = 'ADD_ISSUE_TO_LIST_FAILURE'; export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE'; export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE'; export const SET_ACTIVE_ID = 'SET_ACTIVE_ID'; +export const UPDATE_ISSUE_BY_ID = 'UPDATE_ISSUE_BY_ID'; diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js index ca9b911ce5b..faeb3e25a71 100644 --- a/app/assets/javascripts/boards/stores/mutations.js +++ b/app/assets/javascripts/boards/stores/mutations.js @@ -1,19 +1,55 @@ +import Vue from 'vue'; +import { sortBy, pull } from 'lodash'; +import { formatIssue, moveIssueListHelper } from '../boards_util'; import * as mutationTypes from './mutation_types'; +import { __ } from '~/locale'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; const notImplemented = () => { /* eslint-disable-next-line @gitlab/require-i18n-strings */ throw new Error('Not implemented!'); }; +const removeIssueFromList = (state, listId, issueId) => { + Vue.set(state.issuesByListId, listId, pull(state.issuesByListId[listId], issueId)); +}; + +const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfterId, atIndex }) => { + const listIssues = state.issuesByListId[listId]; + let newIndex = atIndex || 0; + if (moveBeforeId) { + newIndex = listIssues.indexOf(moveBeforeId) + 1; + } else if (moveAfterId) { + newIndex = listIssues.indexOf(moveAfterId); + } + listIssues.splice(newIndex, 0, issueId); + Vue.set(state.issuesByListId, listId, listIssues); +}; + export default { - [mutationTypes.SET_INITIAL_BOARD_DATA]: (state, data) => { - const { boardType, ...endpoints } = data; + [mutationTypes.SET_INITIAL_BOARD_DATA](state, data) { + const { boardType, disabled, showPromotion, ...endpoints } = data; state.endpoints = endpoints; state.boardType = boardType; + state.disabled = disabled; + state.showPromotion = showPromotion; }, - [mutationTypes.SET_ACTIVE_ID](state, id) { + [mutationTypes.RECEIVE_BOARD_LISTS_SUCCESS]: (state, lists) => { + state.boardLists = lists; + }, + + [mutationTypes.SET_ACTIVE_ID](state, { id, sidebarType }) { state.activeId = id; + state.sidebarType = sidebarType; + }, + + [mutationTypes.SET_FILTERS](state, filterParams) { + state.filterParams = filterParams; + }, + + [mutationTypes.CREATE_LIST_FAILURE]: state => { + state.error = __('An error occurred while creating the list. Please try again.'); }, [mutationTypes.REQUEST_ADD_LIST]: () => { @@ -28,16 +64,17 @@ export default { notImplemented(); }, - [mutationTypes.REQUEST_UPDATE_LIST]: () => { - notImplemented(); - }, - - [mutationTypes.RECEIVE_UPDATE_LIST_SUCCESS]: () => { - notImplemented(); + [mutationTypes.MOVE_LIST]: (state, { movedList, listAtNewIndex }) => { + const { boardLists } = state; + const movedListIndex = state.boardLists.findIndex(l => l.id === movedList.id); + Vue.set(boardLists, movedListIndex, movedList); + Vue.set(boardLists, movedListIndex.position + 1, listAtNewIndex); + Vue.set(state, 'boardLists', sortBy(boardLists, 'position')); }, - [mutationTypes.RECEIVE_UPDATE_LIST_ERROR]: () => { - notImplemented(); + [mutationTypes.UPDATE_LIST_FAILURE]: (state, backupList) => { + state.error = __('An error occurred while updating the list. Please try again.'); + Vue.set(state, 'boardLists', backupList); }, [mutationTypes.REQUEST_REMOVE_LIST]: () => { @@ -52,17 +89,41 @@ export default { notImplemented(); }, + [mutationTypes.RECEIVE_ISSUES_FOR_LIST_SUCCESS]: (state, { listIssues, listId }) => { + const { listData, issues } = listIssues; + Vue.set(state, 'issues', { ...state.issues, ...issues }); + Vue.set(state.issuesByListId, listId, listData[listId]); + const listIndex = state.boardLists.findIndex(l => l.id === listId); + Vue.set(state.boardLists[listIndex], 'loading', false); + }, + + [mutationTypes.RECEIVE_ISSUES_FOR_LIST_FAILURE]: (state, listId) => { + state.error = __('An error occurred while fetching the board issues. Please reload the page.'); + const listIndex = state.boardLists.findIndex(l => l.id === listId); + Vue.set(state.boardLists[listIndex], 'loading', false); + }, + [mutationTypes.REQUEST_ISSUES_FOR_ALL_LISTS]: state => { state.isLoadingIssues = true; }, - [mutationTypes.RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS]: (state, listIssues) => { - state.issuesByListId = listIssues; + [mutationTypes.RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS]: (state, { listData, issues }) => { + state.issuesByListId = listData; + state.issues = issues; state.isLoadingIssues = false; }, + [mutationTypes.UPDATE_ISSUE_BY_ID]: (state, { issueId, prop, value }) => { + if (!state.issues[issueId]) { + /* eslint-disable-next-line @gitlab/require-i18n-strings */ + throw new Error('No issue found.'); + } + + Vue.set(state.issues[issueId], prop, value); + }, + [mutationTypes.RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE]: state => { - state.listIssueFetchFailure = true; + state.error = __('An error occurred while fetching the board issues. Please reload the page.'); state.isLoadingIssues = false; }, @@ -78,16 +139,38 @@ export default { notImplemented(); }, - [mutationTypes.REQUEST_MOVE_ISSUE]: () => { - notImplemented(); + [mutationTypes.MOVE_ISSUE]: ( + state, + { originalIssue, fromListId, toListId, moveBeforeId, moveAfterId }, + ) => { + const fromList = state.boardLists.find(l => l.id === fromListId); + const toList = state.boardLists.find(l => l.id === toListId); + + const issue = moveIssueListHelper(originalIssue, fromList, toList); + Vue.set(state.issues, issue.id, issue); + + removeIssueFromList(state, fromListId, issue.id); + addIssueToList({ state, listId: toListId, issueId: issue.id, moveBeforeId, moveAfterId }); }, - [mutationTypes.RECEIVE_MOVE_ISSUE_SUCCESS]: () => { - notImplemented(); + [mutationTypes.MOVE_ISSUE_SUCCESS]: (state, { issue }) => { + const issueId = getIdFromGraphQLId(issue.id); + Vue.set(state.issues, issueId, formatIssue({ ...issue, id: issueId })); }, - [mutationTypes.RECEIVE_MOVE_ISSUE_ERROR]: () => { - notImplemented(); + [mutationTypes.MOVE_ISSUE_FAILURE]: ( + state, + { originalIssue, fromListId, toListId, originalIndex }, + ) => { + state.error = __('An error occurred while moving the issue. Please try again.'); + Vue.set(state.issues, originalIssue.id, originalIssue); + removeIssueFromList(state, toListId, originalIssue.id); + addIssueToList({ + state, + listId: fromListId, + issueId: originalIssue.id, + atIndex: originalIndex, + }); }, [mutationTypes.REQUEST_UPDATE_ISSUE]: () => { @@ -102,6 +185,18 @@ export default { notImplemented(); }, + [mutationTypes.ADD_ISSUE_TO_LIST]: (state, { list, issue, position }) => { + const listIssues = state.issuesByListId[list.id]; + listIssues.splice(position, 0, issue.id); + Vue.set(state.issuesByListId, list.id, listIssues); + Vue.set(state.issues, issue.id, issue); + }, + + [mutationTypes.ADD_ISSUE_TO_LIST_FAILURE]: (state, { list, issue }) => { + state.error = __('An error occurred while creating the issue. Please try again.'); + removeIssueFromList(state, list.id, issue.id); + }, + [mutationTypes.SET_CURRENT_PAGE]: () => { notImplemented(); }, diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js index cb6930774ed..be937d68c6c 100644 --- a/app/assets/javascripts/boards/stores/state.js +++ b/app/assets/javascripts/boards/stores/state.js @@ -3,9 +3,17 @@ import { inactiveId } from '~/boards/constants'; export default () => ({ endpoints: {}, boardType: null, + disabled: false, + showPromotion: false, isShowingLabels: true, activeId: inactiveId, + sidebarType: '', + boardLists: [], issuesByListId: {}, + issues: {}, isLoadingIssues: false, - listIssueFetchFailure: false, + filterParams: {}, + error: undefined, + // TODO: remove after ce/ee split of board_content.vue + isShowingEpicsSwimlanes: false, }); diff --git a/app/assets/javascripts/branches/ajax_loading_spinner.js b/app/assets/javascripts/branches/ajax_loading_spinner.js new file mode 100644 index 00000000000..79f4f919f3d --- /dev/null +++ b/app/assets/javascripts/branches/ajax_loading_spinner.js @@ -0,0 +1,31 @@ +import $ from 'jquery'; + +export default class AjaxLoadingSpinner { + static init() { + const $elements = $('.js-ajax-loading-spinner'); + $elements.on('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend); + } + + static ajaxBeforeSend(e) { + const button = e.target; + const newButton = document.createElement('button'); + newButton.classList.add('btn', 'btn-default', 'disabled', 'gl-button'); + newButton.setAttribute('disabled', 'disabled'); + + const spinner = document.createElement('span'); + spinner.classList.add('align-text-bottom', 'gl-spinner', 'gl-spinner-sm', 'gl-spinner-orange'); + newButton.appendChild(spinner); + + button.classList.add('hidden'); + button.parentNode.insertBefore(newButton, button.nextSibling); + + $(button).one('ajax:error', () => { + newButton.remove(); + button.classList.remove('hidden'); + }); + + $(button).one('ajax:success', () => { + $(button).off('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend); + }); + } +} diff --git a/app/assets/javascripts/ci_lint/components/ci_lint.vue b/app/assets/javascripts/ci_lint/components/ci_lint.vue new file mode 100644 index 00000000000..135d02e4f76 --- /dev/null +++ b/app/assets/javascripts/ci_lint/components/ci_lint.vue @@ -0,0 +1,14 @@ +<script> +export default { + props: { + endpoint: { + type: String, + required: true, + }, + }, +}; +</script> + +<template + ><div></div +></template> diff --git a/app/assets/javascripts/ci_lint/components/ci_lint_results.vue b/app/assets/javascripts/ci_lint/components/ci_lint_results.vue new file mode 100644 index 00000000000..9fd1bd30c49 --- /dev/null +++ b/app/assets/javascripts/ci_lint/components/ci_lint_results.vue @@ -0,0 +1,9 @@ +<script> +export default { + props: {}, +}; +</script> + +<template + ><div></div +></template> diff --git a/app/assets/javascripts/ci_lint/index.js b/app/assets/javascripts/ci_lint/index.js new file mode 100644 index 00000000000..ed2cf1fe714 --- /dev/null +++ b/app/assets/javascripts/ci_lint/index.js @@ -0,0 +1,18 @@ +import Vue from 'vue'; +import CILint from './components/ci_lint.vue'; + +export default (containerId = '#js-ci-lint') => { + const containerEl = document.querySelector(containerId); + const { endpoint } = containerEl.dataset; + + return new Vue({ + el: containerEl, + render(createElement) { + return createElement(CILint, { + props: { + endpoint, + }, + }); + }, + }); +}; 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 5c79f245f6d..cb1935c863d 100644 --- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js +++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js @@ -53,7 +53,7 @@ export default class VariableList { }, environment_scope: { // We can't use a `.js-` class here because - // gl_dropdown replaces the <input> and doesn't copy over the class + // deprecated_jquery_dropdown replaces the <input> and doesn't copy over the class // See https://gitlab.com/gitlab-org/gitlab-foss/issues/42458 selector: `input[name="${this.formField}[variables_attributes][][environment_scope]"]`, default: '*', 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 d22fef27964..0e09ae108ea 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 @@ -67,7 +67,7 @@ export default { </script> <template> <gl-deprecated-dropdown :text="value"> - <gl-search-box-by-type v-model.trim="searchTerm" class="m-2" /> + <gl-search-box-by-type v-model.trim="searchTerm" class="gl-m-3" /> <gl-deprecated-dropdown-item v-for="environment in filteredResults" :key="environment" 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 018704bff74..501c82b419e 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 @@ -46,6 +46,7 @@ export default { { key: 'actions', label: '', + tdClass: 'text-right', customStyle: { width: '35px' }, }, ], diff --git a/app/assets/javascripts/ci_variable_list/constants.js b/app/assets/javascripts/ci_variable_list/constants.js index ef304c7ccee..564e1d01242 100644 --- a/app/assets/javascripts/ci_variable_list/constants.js +++ b/app/assets/javascripts/ci_variable_list/constants.js @@ -1,6 +1,5 @@ import { __ } from '~/locale'; -// eslint-disable import/prefer-default-export export const ADD_CI_VARIABLE_MODAL_ID = 'add-ci-variable'; export const displayText = { diff --git a/app/assets/javascripts/ci_variable_list/store/getters.js b/app/assets/javascripts/ci_variable_list/store/getters.js index 14b728302f9..619ad54cad6 100644 --- a/app/assets/javascripts/ci_variable_list/store/getters.js +++ b/app/assets/javascripts/ci_variable_list/store/getters.js @@ -1,6 +1,3 @@ -/* eslint-disable import/prefer-default-export */ -// Disabling import/prefer-default-export can be -// removed once a second getter is added to this file import { uniq } from 'lodash'; export const joinedEnvironments = state => { diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 92517203972..a75646db162 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -237,7 +237,7 @@ export default class Clusters { } addBannerCloseHandler(el, status) { - el.querySelector('.js-close-banner').addEventListener('click', () => { + el.querySelector('.js-close').addEventListener('click', () => { el.classList.add('hidden'); this.setBannerDismissedState(status, true); }); diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index c86db28515f..412260da958 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -1,9 +1,8 @@ <script> -import { GlLink, GlModalDirective, GlSprintf } from '@gitlab/ui'; +import { GlLink, GlModalDirective, GlSprintf, GlButton, GlAlert } from '@gitlab/ui'; import { s__, __, sprintf } from '~/locale'; import eventHub from '../event_hub'; import identicon from '../../vue_shared/components/identicon.vue'; -import loadingButton from '../../vue_shared/components/loading_button.vue'; import UninstallApplicationButton from './uninstall_application_button.vue'; import UninstallApplicationConfirmationModal from './uninstall_application_confirmation_modal.vue'; import UpdateApplicationConfirmationModal from './update_application_confirmation_modal.vue'; @@ -12,9 +11,10 @@ import { APPLICATION_STATUS, ELASTIC_STACK } from '../constants'; export default { components: { - loadingButton, + GlButton, identicon, GlLink, + GlAlert, GlSprintf, UninstallApplicationButton, UninstallApplicationConfirmationModal, @@ -382,24 +382,28 @@ export default { </template> </div> - <div + <gl-alert v-if="updateFailed && !isUpdating" - class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-update-details" + variant="danger" + :dismissible="false" + class="gl-mt-3 gl-mb-0 js-cluster-application-update-details" > {{ updateFailureDescription }} - </div> + </gl-alert> <template v-if="updateAvailable || updateFailed || isUpdating"> <template v-if="updatingNeedsConfirmation"> - <loading-button + <gl-button v-gl-modal-directive="updateModalId" - class="btn btn-primary js-cluster-application-update-button mt-2" + class="js-cluster-application-update-button mt-2" + variant="info" + category="primary" :loading="isUpdating" :disabled="isUpdating" - :label="updateButtonLabel" data-qa-selector="update_button_with_confirmation" :data-qa-application="id" - /> - + > + {{ updateButtonLabel }} + </gl-button> <update-application-confirmation-modal :application="id" :application-title="title" @@ -407,16 +411,19 @@ export default { /> </template> - <loading-button + <gl-button v-else - class="btn btn-primary js-cluster-application-update-button mt-2" + class="js-cluster-application-update-button mt-2" + variant="info" + category="primary" :loading="isUpdating" :disabled="isUpdating" - :label="updateButtonLabel" data-qa-selector="update_button" :data-qa-application="id" @click="updateConfirmed" - /> + > + {{ updateButtonLabel }} + </gl-button> </template> </div> </div> @@ -431,16 +438,18 @@ export default { }}</a> </div> <div class="btn-group table-action-buttons"> - <loading-button + <gl-button v-if="displayInstallButton" :loading="installButtonLoading" :disabled="disabled || installButtonDisabled" - :label="installButtonLabel" class="js-cluster-application-install-button" + variant="default" data-qa-selector="install_button" :data-qa-application="id" @click="installClicked" - /> + > + {{ installButtonLabel }} + </gl-button> <uninstall-application-button v-if="displayUninstallButton" v-gl-modal-directive="uninstallModalId" diff --git a/app/assets/javascripts/clusters/components/knative_domain_editor.vue b/app/assets/javascripts/clusters/components/knative_domain_editor.vue index 1236d2a46c9..2617ea0bdea 100644 --- a/app/assets/javascripts/clusters/components/knative_domain_editor.vue +++ b/app/assets/javascripts/clusters/components/knative_domain_editor.vue @@ -6,8 +6,8 @@ import { GlLoadingIcon, GlSearchBoxByType, GlSprintf, + GlButton, } from '@gitlab/ui'; -import LoadingButton from '~/vue_shared/components/loading_button.vue'; import ClipboardButton from '../../vue_shared/components/clipboard_button.vue'; import { __, s__ } from '~/locale'; @@ -17,7 +17,7 @@ const { UPDATING, UNINSTALLING } = APPLICATION_STATUS; export default { components: { - LoadingButton, + GlButton, ClipboardButton, GlLoadingIcon, GlDeprecatedDropdown, @@ -130,7 +130,7 @@ export default { <gl-search-box-by-type v-model.trim="searchQuery" :placeholder="s__('ClusterIntegration|Search domains')" - class="m-2" + class="gl-m-3" /> <gl-deprecated-dropdown-item v-for="domain in filteredDomains" @@ -215,13 +215,16 @@ export default { }} </p> - <loading-button - class="btn-success js-knative-save-domain-button mt-3 ml-3" + <gl-button + class="js-knative-save-domain-button gl-mt-5 gl-ml-5" + variant="success" + category="primary" :loading="saving" :disabled="saveButtonDisabled" - :label="saveButtonLabel" @click="$emit('save')" - /> + > + {{ saveButtonLabel }} + </gl-button> </template> </div> </template> diff --git a/app/assets/javascripts/clusters/components/new_cluster.vue b/app/assets/javascripts/clusters/components/new_cluster.vue new file mode 100644 index 00000000000..2e74ad073c5 --- /dev/null +++ b/app/assets/javascripts/clusters/components/new_cluster.vue @@ -0,0 +1,34 @@ +<script> +import { GlLink, GlSprintf } from '@gitlab/ui'; +import { mapState } from 'vuex'; +import { s__ } from '~/locale'; + +export default { + i18n: { + title: s__('ClusterIntegration|Enter the details for your Kubernetes cluster'), + information: s__( + 'ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{linkStart}documentation%{linkEnd} on Kubernetes', + ), + }, + components: { + GlLink, + GlSprintf, + }, + computed: { + ...mapState(['clusterConnectHelpPath']), + }, +}; +</script> + +<template> + <div> + <h4>{{ $options.i18n.title }}</h4> + <p> + <gl-sprintf :message="$options.i18n.information"> + <template #link="{ content }"> + <gl-link :href="clusterConnectHelpPath" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </p> + </div> +</template> diff --git a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue index 3e3b102f0aa..c157b04b4f5 100644 --- a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue +++ b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue @@ -1,6 +1,7 @@ <script> +/* eslint-disable vue/no-v-html */ import { escape } from 'lodash'; -import { GlModal, GlButton, GlDeprecatedButton, GlFormInput, GlSprintf } from '@gitlab/ui'; +import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui'; import SplitButton from '~/vue_shared/components/split_button.vue'; import { s__, sprintf } from '~/locale'; import csrf from '~/lib/utils/csrf'; @@ -28,7 +29,6 @@ export default { SplitButton, GlModal, GlButton, - GlDeprecatedButton, GlFormInput, GlSprintf, }, @@ -174,24 +174,31 @@ export default { }}</span> </template> <template #modal-footer> - <gl-deprecated-button variant="secondary" @click="handleCancel">{{ - s__('Cancel') - }}</gl-deprecated-button> + <gl-button variant="secondary" @click="handleCancel">{{ s__('Cancel') }}</gl-button> <template v-if="confirmCleanup"> - <gl-deprecated-button :disabled="!canSubmit" variant="warning" @click="handleSubmit">{{ - s__('ClusterIntegration|Remove integration') - }}</gl-deprecated-button> - <gl-deprecated-button + <gl-button + :disabled="!canSubmit" + variant="warning" + category="primary" + @click="handleSubmit" + >{{ s__('ClusterIntegration|Remove integration') }}</gl-button + > + <gl-button :disabled="!canSubmit" variant="danger" + category="primary" @click="handleSubmit(true)" - >{{ s__('ClusterIntegration|Remove integration and resources') }}</gl-deprecated-button + >{{ s__('ClusterIntegration|Remove integration and resources') }}</gl-button > </template> <template v-else> - <gl-deprecated-button :disabled="!canSubmit" variant="danger" @click="handleSubmit">{{ - s__('ClusterIntegration|Remove integration') - }}</gl-deprecated-button> + <gl-button + :disabled="!canSubmit" + variant="danger" + category="primary" + @click="handleSubmit" + >{{ s__('ClusterIntegration|Remove integration') }}</gl-button + > </template> </template> </gl-modal> diff --git a/app/assets/javascripts/clusters/components/uninstall_application_button.vue b/app/assets/javascripts/clusters/components/uninstall_application_button.vue index 8465312d84d..73191d6d84d 100644 --- a/app/assets/javascripts/clusters/components/uninstall_application_button.vue +++ b/app/assets/javascripts/clusters/components/uninstall_application_button.vue @@ -1,5 +1,5 @@ <script> -import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import { GlButton } from '@gitlab/ui'; import { APPLICATION_STATUS } from '~/clusters/constants'; import { __ } from '~/locale'; @@ -7,7 +7,7 @@ const { UPDATING, UNINSTALLING } = APPLICATION_STATUS; export default { components: { - LoadingButton, + GlButton, }, props: { status: { @@ -30,5 +30,7 @@ export default { </script> <template> - <loading-button :label="label" :disabled="disabled" :loading="loading" /> + <gl-button :disabled="disabled" variant="default" :loading="loading"> + {{ label }} + </gl-button> </template> diff --git a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue index e33431d2ea1..f82f4dd5012 100644 --- a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue +++ b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { GlModal } from '@gitlab/ui'; import trackUninstallButtonClickMixin from 'ee_else_ce/clusters/mixins/track_uninstall_button_click'; import { sprintf, s__ } from '~/locale'; diff --git a/app/assets/javascripts/clusters/components/update_application_confirmation_modal.vue b/app/assets/javascripts/clusters/components/update_application_confirmation_modal.vue index 04aa28e9b74..0aedc6e84fa 100644 --- a/app/assets/javascripts/clusters/components/update_application_confirmation_modal.vue +++ b/app/assets/javascripts/clusters/components/update_application_confirmation_modal.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { GlModal } from '@gitlab/ui'; import { sprintf, s__ } from '~/locale'; import { ELASTIC_STACK } from '../constants'; diff --git a/app/assets/javascripts/clusters/new_cluster.js b/app/assets/javascripts/clusters/new_cluster.js new file mode 100644 index 00000000000..71f585fd307 --- /dev/null +++ b/app/assets/javascripts/clusters/new_cluster.js @@ -0,0 +1,19 @@ +import Vue from 'vue'; +import NewCluster from './components/new_cluster.vue'; +import { createStore } from './stores/new_cluster'; + +export default () => { + const entryPoint = document.querySelector('#js-cluster-new'); + + if (!entryPoint) { + return null; + } + + return new Vue({ + el: '#js-cluster-new', + store: createStore(entryPoint.dataset), + render(createElement) { + return createElement(NewCluster); + }, + }); +}; diff --git a/app/assets/javascripts/clusters/stores/new_cluster/index.js b/app/assets/javascripts/clusters/stores/new_cluster/index.js new file mode 100644 index 00000000000..ae082c07f26 --- /dev/null +++ b/app/assets/javascripts/clusters/stores/new_cluster/index.js @@ -0,0 +1,12 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import state from './state'; + +Vue.use(Vuex); + +export const createStore = initialState => + new Vuex.Store({ + state: state(initialState), + }); + +export default createStore; diff --git a/app/assets/javascripts/clusters/stores/new_cluster/state.js b/app/assets/javascripts/clusters/stores/new_cluster/state.js new file mode 100644 index 00000000000..1ca1ac8de18 --- /dev/null +++ b/app/assets/javascripts/clusters/stores/new_cluster/state.js @@ -0,0 +1,3 @@ +export default (initialState = {}) => ({ + clusterConnectHelpPath: initialState.clusterConnectHelpPath, +}); diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue index 09d7c0329a9..7b53020fc49 100644 --- a/app/assets/javascripts/clusters_list/components/clusters.vue +++ b/app/assets/javascripts/clusters_list/components/clusters.vue @@ -5,7 +5,7 @@ import { GlLink, GlLoadingIcon, GlPagination, - GlSkeletonLoading, + GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf, GlTable, } from '@gitlab/ui'; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index 1526d994770..2f4118c1717 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -247,7 +247,7 @@ export default { }} </p> <gl-link - href="/help/ci/merge_request_pipelines/index.html#create-pipelines-in-the-parent-project-for-merge-requests-from-a-forked-project" + href="/help/ci/merge_request_pipelines/index.html#run-pipelines-in-the-parent-project-for-merge-requests-from-a-forked-project" target="_blank" > {{ s__('Pipelines|More Information') }} diff --git a/app/assets/javascripts/commons/jquery.js b/app/assets/javascripts/commons/jquery.js index c4d663dfc8d..334f95bb27f 100644 --- a/app/assets/javascripts/commons/jquery.js +++ b/app/assets/javascripts/commons/jquery.js @@ -2,6 +2,3 @@ import 'jquery'; // common jQuery plugins import 'jquery-ujs'; -import 'jquery.caret'; // must be imported before at.js -import '@gitlab/at.js'; -import 'vendor/jquery.scrollTo'; diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js index 34322755fe9..45ac1bafd61 100644 --- a/app/assets/javascripts/compare_autocomplete.js +++ b/app/assets/javascripts/compare_autocomplete.js @@ -5,6 +5,7 @@ import { __ } from './locale'; import axios from './lib/utils/axios_utils'; import { deprecatedCreateFlash as flash } from './flash'; import { capitalizeFirstCharacter } from './lib/utils/text_utility'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) { $('.js-compare-dropdown').each(function() { @@ -13,7 +14,7 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = ( const $dropdownContainer = $dropdown.closest('.dropdown'); const $fieldInput = $(`input[name="${$dropdown.data('fieldName')}"]`, $dropdownContainer); const $filterInput = $('input[type="search"]', $dropdownContainer); - $dropdown.glDropdown({ + initDeprecatedJQueryDropdown($dropdown, { data(term, callback) { const params = { ref: $dropdown.data('ref'), diff --git a/app/assets/javascripts/confidential_merge_request/components/dropdown.vue b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue index b8163ecfab2..5b4bdca46e4 100644 --- a/app/assets/javascripts/confidential_merge_request/components/dropdown.vue +++ b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue @@ -1,13 +1,12 @@ <script> -import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui'; +import { GlDeprecatedDropdown, GlDeprecatedDropdownItem, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; export default { components: { GlDeprecatedDropdown, GlDeprecatedDropdownItem, - Icon, + GlIcon, }, props: { projects: { @@ -41,17 +40,17 @@ export default { <gl-deprecated-dropdown toggle-class="d-flex align-items-center w-100" class="w-100"> <template #button-content> <span class="str-truncated-100 mr-2"> - <icon name="lock" /> + <gl-icon name="lock" /> {{ dropdownText }} </span> - <icon name="chevron-down" class="ml-auto" /> + <gl-icon name="chevron-down" class="ml-auto" /> </template> <gl-deprecated-dropdown-item v-for="project in projects" :key="project.id" @click="selectProject(project)" > - <icon + <gl-icon name="mobile-issue-close" :class="{ icon: project.id !== selectedProject.id }" class="js-active-project-check" 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 88459d5962e..3a6707bc573 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 @@ -1,5 +1,5 @@ <script> -import { GlLink, GlSprintf } from '@gitlab/ui'; +import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; import { __ } from '../../locale'; import { deprecatedCreateFlash as createFlash } from '../../flash'; import Api from '../../api'; @@ -8,6 +8,7 @@ import Dropdown from './dropdown.vue'; export default { components: { + GlIcon, GlLink, GlSprintf, Dropdown, @@ -136,7 +137,7 @@ export default { target="_blank" > <span class="sr-only">{{ __('Read more') }}</span> - <i class="fa fa-question-circle" aria-hidden="true"></i> + <gl-icon name="question-o" /> </gl-link> </p> </div> diff --git a/app/assets/javascripts/contributors/stores/actions.js b/app/assets/javascripts/contributors/stores/actions.js index 393af932fb0..f941c5aa944 100644 --- a/app/assets/javascripts/contributors/stores/actions.js +++ b/app/assets/javascripts/contributors/stores/actions.js @@ -3,7 +3,6 @@ import { __ } from '~/locale'; import service from '../services/contributors_service'; import * as types from './mutation_types'; -// eslint-disable-next-line import/prefer-default-export export const fetchChartData = ({ commit }, endpoint) => { commit(types.SET_LOADING_STATE, true); diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue index d6c402fcb5d..a653e228e3f 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue +++ b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { createNamespacedHelpers, mapState, mapActions, mapGetters } from 'vuex'; import { escape } from 'lodash'; import { GlFormInput, GlFormCheckbox } from '@gitlab/ui'; diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue index e063f9edfd9..5c13cbb2775 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue +++ b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue @@ -1,15 +1,15 @@ <script> -import { GlFormInput } from '@gitlab/ui'; +/* eslint-disable vue/no-v-html */ +import { GlFormInput, GlButton } from '@gitlab/ui'; import { escape } from 'lodash'; import { mapState, mapActions } from 'vuex'; import { sprintf, s__, __ } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import LoadingButton from '~/vue_shared/components/loading_button.vue'; export default { components: { GlFormInput, - LoadingButton, + GlButton, ClipboardButton, }, props: { @@ -130,13 +130,15 @@ export default { <gl-form-input id="eks-provision-role-arn" v-model="roleArn" /> <p class="form-text text-muted" v-html="provisionRoleArnHelpText"></p> </div> - <loading-button - class="js-submit-service-credentials btn-success" + <gl-button + variant="success" + category="primary" type="submit" :disabled="submitButtonDisabled" :loading="isCreatingRole" - :label="submitButtonLabel" @click.prevent="createRole({ roleArn, externalId })" - /> + > + {{ submitButtonLabel }} + </gl-button> </form> </template> diff --git a/app/assets/javascripts/create_cluster/eks_cluster/constants.js b/app/assets/javascripts/create_cluster/eks_cluster/constants.js index a850ba89818..471d6e1f0aa 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/constants.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/constants.js @@ -1,2 +1,6 @@ -// eslint-disable-next-line import/prefer-default-export -export const KUBERNETES_VERSIONS = [{ name: '1.14', value: '1.14' }]; +export const KUBERNETES_VERSIONS = [ + { name: '1.14', value: '1.14' }, + { name: '1.15', value: '1.15' }, + { name: '1.16', value: '1.16', default: true }, + { name: '1.17', value: '1.17' }, +]; 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 caf2729a4c7..5abff3c7831 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js @@ -56,6 +56,7 @@ export const createCluster = ({ dispatch, state }) => { environment_scope: state.environmentScope, managed: state.gitlabManagedCluster, provider_aws_attributes: { + kubernetes_version: state.kubernetesVersion, region: state.selectedRegion, vpc_id: state.selectedVpc, subnet_ids: state.selectedSubnet, diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/getters.js b/app/assets/javascripts/create_cluster/eks_cluster/store/getters.js index bbe4930c191..d8489ca31cf 100644 --- a/app/assets/javascripts/create_cluster/eks_cluster/store/getters.js +++ b/app/assets/javascripts/create_cluster/eks_cluster/store/getters.js @@ -1,3 +1,2 @@ -// eslint-disable-next-line import/prefer-default-export export const subnetValid = ({ selectedSubnet }) => Array.isArray(selectedSubnet) && selectedSubnet.length >= 2; 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 d1337e7ea4a..ed51e95e434 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 [{ value: kubernetesVersion }] = KUBERNETES_VERSIONS; +const kubernetesVersion = KUBERNETES_VERSIONS.find(version => version.default).value; export default () => ({ createRolePath: null, diff --git a/app/assets/javascripts/create_item_dropdown.js b/app/assets/javascripts/create_item_dropdown.js index ec09dafebcb..75e8523adfa 100644 --- a/app/assets/javascripts/create_item_dropdown.js +++ b/app/assets/javascripts/create_item_dropdown.js @@ -1,5 +1,5 @@ import { escape } from 'lodash'; -import '~/gl_dropdown'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default class CreateItemDropdown { /** @@ -28,7 +28,7 @@ export default class CreateItemDropdown { } buildDropdown() { - this.$dropdown.glDropdown({ + initDeprecatedJQueryDropdown(this.$dropdown, { data: this.getData.bind(this), filterable: true, filterRemote: this.getDataRemote, @@ -67,12 +67,12 @@ export default class CreateItemDropdown { e.preventDefault(); this.refreshData(); - this.$dropdown.data('glDropdown').selectRowAtIndex(); + this.$dropdown.data('deprecatedJQueryDropdown').selectRowAtIndex(); } refreshData() { // Refresh the dropdown's data, which ends up calling `getData` - this.$dropdown.data('glDropdown').remote.execute(); + this.$dropdown.data('deprecatedJQueryDropdown').remote.execute(); } getData(term, callback) { 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 f5207b47f69..6f8455e4bcf 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 @@ -1,8 +1,14 @@ <script> -import { GlFormInput, GlLink, GlFormGroup, GlFormRadioGroup, GlLoadingIcon } from '@gitlab/ui'; +import { + GlFormInput, + GlLink, + GlFormGroup, + GlFormRadioGroup, + GlLoadingIcon, + GlIcon, +} from '@gitlab/ui'; import { debounce } from 'lodash'; import { __, s__ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; import csrf from '~/lib/utils/csrf'; import axios from '~/lib/utils/axios_utils'; import statusCodes from '~/lib/utils/http_status'; @@ -37,7 +43,7 @@ export default { GlFormGroup, GlFormRadioGroup, GlLoadingIcon, - Icon, + GlIcon, }, props: { formOperation: { @@ -229,7 +235,7 @@ export default { {{ s__('Metrics|Must be a valid PromQL query.') }} <gl-link href="https://prometheus.io/docs/prometheus/latest/querying/basics/" tabindex="-1"> {{ s__('Metrics|Prometheus Query Documentation') }} - <icon name="external-link" :size="12" /> + <gl-icon name="external-link" :size="12" /> </gl-link> </span> </gl-form-group> diff --git a/app/assets/javascripts/cycle_analytics/components/banner.vue b/app/assets/javascripts/cycle_analytics/components/banner.vue index 0db9d2dbcf9..4448d909c9b 100644 --- a/app/assets/javascripts/cycle_analytics/components/banner.vue +++ b/app/assets/javascripts/cycle_analytics/components/banner.vue @@ -1,10 +1,11 @@ <script> +/* eslint-disable vue/no-v-html */ import iconCycleAnalyticsSplash from 'icons/_icon_cycle_analytics_splash.svg'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; export default { components: { - Icon, + GlIcon, }, props: { documentationLink: { @@ -32,7 +33,7 @@ export default { type="button" @click="dismissOverviewDialog" > - <icon name="close" /> + <gl-icon name="close" /> </button> <div class="svg-container" v-html="iconCycleAnalyticsSplash"></div> <div class="inner-content"> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue index dc13f409462..33b4e649ab0 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue @@ -3,14 +3,12 @@ import { GlIcon } from '@gitlab/ui'; import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; import limitWarning from './limit_warning_component.vue'; import totalTime from './total_time_component.vue'; -import icon from '../../vue_shared/components/icon.vue'; export default { components: { userAvatarImage, totalTime, limitWarning, - icon, GlIcon, }, props: { @@ -60,7 +58,7 @@ export default { </template> <template v-else> <span v-if="mergeRequest.branch" class="merge-request-branch"> - <icon :size="16" name="fork" /> + <gl-icon :size="16" name="fork" /> <a :href="mergeRequest.branch.url"> {{ mergeRequest.branch.name }} </a> </span> </template> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue index 2a507b7e601..ba2be2e5167 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue @@ -1,16 +1,17 @@ <script> +/* eslint-disable vue/no-v-html */ +import { GlIcon } from '@gitlab/ui'; import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; import iconBranch from '../svg/icon_branch.svg'; import limitWarning from './limit_warning_component.vue'; import totalTime from './total_time_component.vue'; -import icon from '../../vue_shared/components/icon.vue'; export default { components: { userAvatarImage, totalTime, limitWarning, - icon, + GlIcon, }, props: { items: { @@ -44,7 +45,7 @@ export default { <user-avatar-image :img-src="build.author.avatarUrl" /> <h5 class="item-title"> <a :href="build.url" class="pipeline-id"> #{{ build.id }} </a> - <icon :size="16" name="fork" /> + <gl-icon :size="16" name="fork" /> <a :href="build.branch.url" class="ref-name"> {{ build.branch.name }} </a> <span class="icon-branch" v-html="iconBranch"> </span> <a :href="build.commitUrl" class="commit-sha"> {{ build.shortSha }} </a> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue index caff6f9c349..cd49b3c5222 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue @@ -1,15 +1,16 @@ <script> +/* eslint-disable vue/no-v-html */ +import { GlIcon } from '@gitlab/ui'; import iconBuildStatus from '../svg/icon_build_status.svg'; import iconBranch from '../svg/icon_branch.svg'; import limitWarning from './limit_warning_component.vue'; import totalTime from './total_time_component.vue'; -import icon from '../../vue_shared/components/icon.vue'; export default { components: { totalTime, limitWarning, - icon, + GlIcon, }, props: { items: { @@ -46,7 +47,7 @@ export default { <span class="icon-build-status" v-html="iconBuildStatus"> </span> <a :href="build.url" class="item-build-name"> {{ build.name }} </a> · <a :href="build.url" class="pipeline-id"> #{{ build.id }} </a> - <icon :size="16" name="fork" /> + <gl-icon :size="16" name="fork" /> <a :href="build.branch.url" class="ref-name"> {{ build.branch.name }} </a> <span class="icon-branch" v-html="iconBranch"> </span> <a :href="build.commitUrl" class="commit-sha"> {{ build.shortSha }} </a> diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js index 4f9069f61a5..3a160d0532c 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js @@ -23,9 +23,6 @@ const EMPTY_STAGE_TEXTS = { staging: __( 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.', ), - production: __( - 'The total stage shows the time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.', - ), }; export default { diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue index a03a7114b40..0ac16e6b6a0 100644 --- a/app/assets/javascripts/deploy_keys/components/app.vue +++ b/app/assets/javascripts/deploy_keys/components/app.vue @@ -1,5 +1,5 @@ <script> -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; import { s__ } from '~/locale'; import { deprecatedCreateFlash as Flash } from '~/flash'; import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue'; @@ -7,14 +7,13 @@ import eventHub from '../eventhub'; import DeployKeysService from '../service'; import DeployKeysStore from '../store'; import KeysPanel from './keys_panel.vue'; -import Icon from '~/vue_shared/components/icon.vue'; export default { components: { KeysPanel, NavigationTabs, GlLoadingIcon, - Icon, + GlIcon, }, props: { endpoint: { @@ -125,8 +124,8 @@ export default { /> <template v-else-if="hasKeys"> <div class="top-area scrolling-tabs-container inner-page-scroll-tabs"> - <div class="fade-left"><icon name="chevron-lg-left" :size="12" /></div> - <div class="fade-right"><icon name="chevron-lg-right" :size="12" /></div> + <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> <navigation-tabs :tabs="tabs" scope="deployKeys" @onChangeTab="onChangeTab" /> </div> diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue index 585b091bc51..5b41d23bd27 100644 --- a/app/assets/javascripts/deploy_keys/components/key.vue +++ b/app/assets/javascripts/deploy_keys/components/key.vue @@ -1,7 +1,7 @@ <script> import { head, tail } from 'lodash'; +import { GlIcon } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; -import icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; import timeagoMixin from '~/vue_shared/mixins/timeago'; @@ -10,7 +10,7 @@ import actionBtn from './action_btn.vue'; export default { components: { actionBtn, - icon, + GlIcon, }, directives: { tooltip, @@ -130,7 +130,7 @@ export default { class="label deploy-project-label" > <span> {{ firstProject.project.full_name }} </span> - <icon :name="firstProject.can_push ? 'lock-open' : 'lock'" /> + <gl-icon :name="firstProject.can_push ? 'lock-open' : 'lock'" /> </a> <a v-if="isExpandable" @@ -151,7 +151,7 @@ export default { class="label deploy-project-label" > <span> {{ deployKeysProject.project.full_name }} </span> - <icon :name="deployKeysProject.can_push ? 'lock-open' : 'lock'" /> + <gl-icon :name="deployKeysProject.can_push ? 'lock-open' : 'lock'" /> </a> </template> <span v-else class="text-secondary">{{ __('None') }}</span> @@ -161,7 +161,7 @@ export default { <div role="rowheader" class="table-mobile-header">{{ __('Created') }}</div> <div class="table-mobile-content text-secondary key-created-at"> <span v-tooltip :title="tooltipTitle(deployKey.created_at)"> - <icon name="calendar" /> <span>{{ timeFormatted(deployKey.created_at) }}</span> + <gl-icon name="calendar" /> <span>{{ timeFormatted(deployKey.created_at) }}</span> </span> </div> </div> @@ -178,7 +178,7 @@ export default { class="btn btn-default text-secondary" data-container="body" > - <icon name="pencil" /> + <gl-icon name="pencil" /> </a> <action-btn v-if="isRemovable" @@ -189,7 +189,7 @@ export default { type="remove" data-container="body" > - <icon name="remove" /> + <gl-icon name="remove" /> </action-btn> <action-btn v-else-if="isEnabled" @@ -200,7 +200,7 @@ export default { type="disable" data-container="body" > - <icon name="cancel" /> + <gl-icon name="cancel" /> </action-btn> </div> </div> diff --git a/app/assets/javascripts/deploy_keys/service/index.js b/app/assets/javascripts/deploy_keys/service/index.js index 268a37008c5..10333752936 100644 --- a/app/assets/javascripts/deploy_keys/service/index.js +++ b/app/assets/javascripts/deploy_keys/service/index.js @@ -2,20 +2,18 @@ import axios from '~/lib/utils/axios_utils'; export default class DeployKeysService { constructor(endpoint) { - this.axios = axios.create({ - baseURL: endpoint, - }); + this.endpoint = endpoint; } getKeys() { - return this.axios.get().then(response => response.data); + return axios.get(this.endpoint).then(response => response.data); } enableKey(id) { - return this.axios.put(`${id}/enable`).then(response => response.data); + return axios.put(`${this.endpoint}/${id}/enable`).then(response => response.data); } disableKey(id) { - return this.axios.put(`${id}/disable`).then(response => response.data); + return axios.put(`${this.endpoint}/${id}/disable`).then(response => response.data); } } diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js index ec0d0cf6aef..c17f2d2efe4 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js @@ -1,16 +1,13 @@ -/* eslint-disable max-classes-per-file, one-var, consistent-return */ - +/* eslint-disable consistent-return */ import $ from 'jquery'; import { escape } from 'lodash'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; -import axios from './lib/utils/axios_utils'; import { visitUrl } from '~/lib/utils/url_utility'; -import { isObject } from './lib/utils/type_utility'; -import renderItem from './gl_dropdown/render'; - -const BLUR_KEYCODES = [27, 40]; - -const HAS_VALUE_CLASS = 'has-value'; +import { isObject } from '~/lib/utils/type_utility'; +import renderItem from './render'; +import { GitLabDropdownRemote } from './gl_dropdown_remote'; +import { GitLabDropdownInput } from './gl_dropdown_input'; +import { GitLabDropdownFilter } from './gl_dropdown_filter'; const LOADING_CLASS = 'is-loading'; @@ -32,216 +29,10 @@ const FILTER_INPUT = '.dropdown-input .dropdown-input-field:not(.dropdown-no-fil const NO_FILTER_INPUT = '.dropdown-input .dropdown-input-field.dropdown-no-filter'; -class GitLabDropdownInput { - constructor(input, options) { - this.input = input; - this.options = options; - this.fieldName = this.options.fieldName || 'field-name'; - const $inputContainer = this.input.parent(); - const $clearButton = $inputContainer.find('.js-dropdown-input-clear'); - $clearButton.on('click', e => { - // Clear click - e.preventDefault(); - e.stopPropagation(); - return this.input - .val('') - .trigger('input') - .focus(); - }); - - this.input - .on('keydown', e => { - const keyCode = e.which; - if (keyCode === 13 && !options.elIsInput) { - e.preventDefault(); - } - }) - .on('input', e => { - let val = e.currentTarget.value || this.options.inputFieldName; - val = val - .split(' ') - .join('-') // replaces space with dash - .replace(/[^a-zA-Z0-9 -]/g, '') - .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); - }); - } - - onInput(cb) { - this.cb = cb; - } -} - -class GitLabDropdownFilter { - constructor(input, options) { - let ref, timeout; - this.input = input; - this.options = options; - // eslint-disable-next-line no-cond-assign - 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 => { - // Clear click - e.preventDefault(); - e.stopPropagation(); - return this.input - .val('') - .trigger('input') - .focus(); - }); - // Key events - timeout = ''; - this.input - .on('keydown', e => { - const keyCode = e.which; - if (keyCode === 13 && !options.elIsInput) { - e.preventDefault(); - } - }) - .on('input', () => { - if (this.input.val() !== '' && !$inputContainer.hasClass(HAS_VALUE_CLASS)) { - $inputContainer.addClass(HAS_VALUE_CLASS); - } else if (this.input.val() === '' && $inputContainer.hasClass(HAS_VALUE_CLASS)) { - $inputContainer.removeClass(HAS_VALUE_CLASS); - } - // Only filter asynchronously only if option remote is set - if (this.options.remote) { - clearTimeout(timeout); - // eslint-disable-next-line no-return-assign - return (timeout = setTimeout(() => { - $inputContainer.parent().addClass('is-loading'); - - return this.options.query(this.input.val(), data => { - $inputContainer.parent().removeClass('is-loading'); - return this.options.callback(data); - }); - }, 250)); - } - return this.filter(this.input.val()); - }); - } - - static shouldBlur(keyCode) { - return BLUR_KEYCODES.indexOf(keyCode) !== -1; - } - - filter(searchText) { - let group, results, tmp; - if (this.options.onFilter) { - this.options.onFilter(searchText); - } - const data = this.options.data(); - if (data != null && !this.options.filterByText) { - results = data; - if (searchText !== '') { - // When data is an array of objects therefore [object Array] e.g. - // [ - // { prop: 'foo' }, - // { prop: 'baz' } - // ] - if (Array.isArray(data)) { - results = fuzzaldrinPlus.filter(data, searchText, { - key: this.options.keys, - }); - } - // If data is grouped therefore an [object Object]. e.g. - // { - // groupName1: [ - // { prop: 'foo' }, - // { prop: 'baz' } - // ], - // groupName2: [ - // { prop: 'abc' }, - // { prop: 'def' } - // ] - // } - else if (isObject(data)) { - results = {}; - 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); - } - }); - } - } - return this.options.callback(results); - } - const elements = this.options.elements(); - if (searchText) { - // eslint-disable-next-line func-names - elements.each(function() { - const $el = $(this); - const matches = fuzzaldrinPlus.match($el.text().trim(), searchText); - if (!$el.is('.dropdown-header')) { - if (matches.length) { - return $el.show().removeClass('option-hidden'); - } - return $el.hide().addClass('option-hidden'); - } - }); - } else { - elements.show().removeClass('option-hidden'); - } - - elements - .parent() - .find('.dropdown-menu-empty-item') - .toggleClass('hidden', elements.is(':visible')); - } -} - -class GitLabDropdownRemote { - constructor(dataEndpoint, options) { - this.dataEndpoint = dataEndpoint; - this.options = options; - } - - execute() { - if (typeof this.dataEndpoint === 'string') { - return this.fetchData(); - } else if (typeof this.dataEndpoint === 'function') { - if (this.options.beforeSend) { - this.options.beforeSend(); - } - return this.dataEndpoint('', data => { - // Fetch the data by calling the data function - if (this.options.success) { - this.options.success(data); - } - if (this.options.beforeSend) { - return this.options.beforeSend(); - } - }); - } - } - - fetchData() { - if (this.options.beforeSend) { - this.options.beforeSend(); - } - - // Fetch the data through ajax if the data is a string - return axios.get(this.dataEndpoint).then(({ data }) => { - if (this.options.success) { - return this.options.success(data); - } - }); - } -} - -class GitLabDropdown { +export class GitLabDropdown { constructor(el1, options) { - let selector, self; + let selector; + let self; this.el = el1; this.options = options; this.updateLabel = this.updateLabel.bind(this); @@ -354,7 +145,8 @@ class GitLabDropdown { } }); this.dropdown.on('blur', 'a', e => { - let $dropdownMenu, $relatedTarget; + let $dropdownMenu; + let $relatedTarget; if (e.relatedTarget != null) { $relatedTarget = $(e.relatedTarget); $dropdownMenu = $relatedTarget.closest('.dropdown-menu'); @@ -421,7 +213,8 @@ class GitLabDropdown { } parseData(data) { - let groupData, html; + let groupData; + let html; this.renderedData = data; if (this.options.filterable && data.length === 0) { // render no matching results @@ -636,7 +429,11 @@ class GitLabDropdown { } rowClicked(el) { - let field, groupName, selectedIndex, selectedObject, isMarking; + let field; + let groupName; + let selectedIndex; + let selectedObject; + let isMarking; const { fieldName } = this.options; const isInput = $(this.el).is('input'); if (this.renderedData) { @@ -790,7 +587,8 @@ class GitLabDropdown { selector = `.dropdown-page-one ${selector}`; } return $('body').on('keydown', e => { - let $listItems, PREV_INDEX; + let $listItems; + let PREV_INDEX; const currentKeyCode = e.which; if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) { e.preventDefault(); @@ -889,13 +687,3 @@ class GitLabDropdown { return isInput ? field.val('') : field.remove(); } } - -// eslint-disable-next-line func-names -$.fn.glDropdown = function(opts) { - // eslint-disable-next-line func-names - return this.each(function() { - if (!$.data(this, 'glDropdown')) { - return $.data(this, 'glDropdown', new GitLabDropdown(this, opts)); - } - }); -}; diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js new file mode 100644 index 00000000000..89ffb5f5f79 --- /dev/null +++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js @@ -0,0 +1,135 @@ +/* eslint-disable consistent-return */ + +import $ from 'jquery'; +import fuzzaldrinPlus from 'fuzzaldrin-plus'; +import { isObject } from '~/lib/utils/type_utility'; + +const BLUR_KEYCODES = [27, 40]; + +const HAS_VALUE_CLASS = 'has-value'; + +export class GitLabDropdownFilter { + constructor(input, options) { + let ref; + let timeout; + this.input = input; + this.options = options; + // eslint-disable-next-line no-cond-assign + 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 => { + // Clear click + e.preventDefault(); + e.stopPropagation(); + return this.input + .val('') + .trigger('input') + .focus(); + }); + // Key events + timeout = ''; + this.input + .on('keydown', e => { + const keyCode = e.which; + if (keyCode === 13 && !options.elIsInput) { + e.preventDefault(); + } + }) + .on('input', () => { + if (this.input.val() !== '' && !$inputContainer.hasClass(HAS_VALUE_CLASS)) { + $inputContainer.addClass(HAS_VALUE_CLASS); + } else if (this.input.val() === '' && $inputContainer.hasClass(HAS_VALUE_CLASS)) { + $inputContainer.removeClass(HAS_VALUE_CLASS); + } + // Only filter asynchronously only if option remote is set + if (this.options.remote) { + clearTimeout(timeout); + // eslint-disable-next-line no-return-assign + return (timeout = setTimeout(() => { + $inputContainer.parent().addClass('is-loading'); + + return this.options.query(this.input.val(), data => { + $inputContainer.parent().removeClass('is-loading'); + return this.options.callback(data); + }); + }, 250)); + } + return this.filter(this.input.val()); + }); + } + + static shouldBlur(keyCode) { + return BLUR_KEYCODES.indexOf(keyCode) !== -1; + } + + filter(searchText) { + let group; + let results; + let tmp; + if (this.options.onFilter) { + this.options.onFilter(searchText); + } + const data = this.options.data(); + if (data != null && !this.options.filterByText) { + results = data; + if (searchText !== '') { + // When data is an array of objects therefore [object Array] e.g. + // [ + // { prop: 'foo' }, + // { prop: 'baz' } + // ] + if (Array.isArray(data)) { + results = fuzzaldrinPlus.filter(data, searchText, { + key: this.options.keys, + }); + } + // If data is grouped therefore an [object Object]. e.g. + // { + // groupName1: [ + // { prop: 'foo' }, + // { prop: 'baz' } + // ], + // groupName2: [ + // { prop: 'abc' }, + // { prop: 'def' } + // ] + // } + else if (isObject(data)) { + results = {}; + 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); + } + }); + } + } + return this.options.callback(results); + } + const elements = this.options.elements(); + if (searchText) { + // eslint-disable-next-line func-names + elements.each(function() { + const $el = $(this); + const matches = fuzzaldrinPlus.match($el.text().trim(), searchText); + if (!$el.is('.dropdown-header')) { + if (matches.length) { + return $el.show().removeClass('option-hidden'); + } + return $el.hide().addClass('option-hidden'); + } + }); + } else { + elements.show().removeClass('option-hidden'); + } + + elements + .parent() + .find('.dropdown-menu-empty-item') + .toggleClass('hidden', elements.is(':visible')); + } +} diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_input.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_input.js new file mode 100644 index 00000000000..d857071d05f --- /dev/null +++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_input.js @@ -0,0 +1,44 @@ +export class GitLabDropdownInput { + constructor(input, options) { + this.input = input; + this.options = options; + this.fieldName = this.options.fieldName || 'field-name'; + const $inputContainer = this.input.parent(); + const $clearButton = $inputContainer.find('.js-dropdown-input-clear'); + $clearButton.on('click', e => { + // Clear click + e.preventDefault(); + e.stopPropagation(); + return this.input + .val('') + .trigger('input') + .focus(); + }); + + this.input + .on('keydown', e => { + const keyCode = e.which; + if (keyCode === 13 && !options.elIsInput) { + e.preventDefault(); + } + }) + .on('input', e => { + let val = e.currentTarget.value || this.options.inputFieldName; + val = val + .split(' ') + .join('-') // replaces space with dash + .replace(/[^a-zA-Z0-9 -]/g, '') + .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); + }); + } + + onInput(cb) { + this.cb = cb; + } +} diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js new file mode 100644 index 00000000000..1f6a2e1f646 --- /dev/null +++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js @@ -0,0 +1,42 @@ +/* eslint-disable consistent-return */ + +import axios from '../lib/utils/axios_utils'; + +export class GitLabDropdownRemote { + constructor(dataEndpoint, options) { + this.dataEndpoint = dataEndpoint; + this.options = options; + } + + execute() { + if (typeof this.dataEndpoint === 'string') { + return this.fetchData(); + } else if (typeof this.dataEndpoint === 'function') { + if (this.options.beforeSend) { + this.options.beforeSend(); + } + return this.dataEndpoint('', data => { + // Fetch the data by calling the data function + if (this.options.success) { + this.options.success(data); + } + if (this.options.beforeSend) { + return this.options.beforeSend(); + } + }); + } + } + + fetchData() { + if (this.options.beforeSend) { + this.options.beforeSend(); + } + + // Fetch the data through ajax if the data is a string + return axios.get(this.dataEndpoint).then(({ data }) => { + if (this.options.success) { + return this.options.success(data); + } + }); + } +} diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/index.js b/app/assets/javascripts/deprecated_jquery_dropdown/index.js new file mode 100644 index 00000000000..90e7f15b5b7 --- /dev/null +++ b/app/assets/javascripts/deprecated_jquery_dropdown/index.js @@ -0,0 +1,11 @@ +import $ from 'jquery'; +import { GitLabDropdown } from './gl_dropdown'; + +export default function initDeprecatedJQueryDropdown($el, opts) { + // eslint-disable-next-line func-names + return $el.each(function() { + if (!$.data(this, 'deprecatedJQueryDropdown')) { + $.data(this, 'deprecatedJQueryDropdown', new GitLabDropdown(this, opts)); + } + }); +} diff --git a/app/assets/javascripts/gl_dropdown/render.js b/app/assets/javascripts/deprecated_jquery_dropdown/render.js index 66546aa834f..167bc4c286e 100644 --- a/app/assets/javascripts/gl_dropdown/render.js +++ b/app/assets/javascripts/deprecated_jquery_dropdown/render.js @@ -1,3 +1,5 @@ +import { slugify } from '~/lib/utils/text_utility'; + const renderersByType = { divider(element) { element.classList.add('divider'); @@ -95,15 +97,22 @@ function checkSelected(data, options) { return options.parent.querySelector(`input[name='${options.fieldName}']`) == null; } -function createLink(url, selected, options) { +function createLink(data, selected, options, index) { const link = document.createElement('a'); - link.href = url; + link.href = getPropertyWithDefault(data, options, 'url', '#'); if (options.icon) { link.classList.add('d-flex', 'align-items-center'); } + if (options.trackSuggestionClickedLabel) { + link.setAttribute('data-track-event', 'click_text'); + link.setAttribute('data-track-label', options.trackSuggestionClickedLabel); + link.setAttribute('data-track-value', index); + link.setAttribute('data-track-property', slugify(data.category || 'no-category')); + } + link.classList.toggle('is-active', selected); return link; @@ -123,8 +132,7 @@ function assignTextToLink(el, data, options) { function renderLink(row, data, { options, group, index }) { const selected = checkSelected(data, options); - const url = getPropertyWithDefault(data, options, 'url', '#'); - const link = createLink(url, selected, options); + const link = createLink(data, selected, options, index); assignTextToLink(link, data, options); diff --git a/app/assets/javascripts/design_management/components/delete_button.vue b/app/assets/javascripts/design_management/components/delete_button.vue index 37686dd5a46..970197ef41b 100644 --- a/app/assets/javascripts/design_management/components/delete_button.vue +++ b/app/assets/javascripts/design_management/components/delete_button.vue @@ -98,6 +98,7 @@ export default { :loading="loading" :icon="buttonIcon" :disabled="isDeleting || !hasSelectedDesigns" - /> + ><slot></slot + ></gl-button> </div> </template> 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 6a20517eed7..845f1aec8cf 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 @@ -1,19 +1,20 @@ <script> import { ApolloMutation } from 'vue-apollo'; -import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink } from '@gitlab/ui'; +import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink, GlBadge } from '@gitlab/ui'; import { s__ } from '~/locale'; +import createFlash from '~/flash'; import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import allVersionsMixin from '../../mixins/all_versions'; import createNoteMutation from '../../graphql/mutations/create_note.mutation.graphql'; import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql'; -import getDesignQuery from '../../graphql/queries/get_design.query.graphql'; import activeDiscussionQuery from '../../graphql/queries/active_discussion.query.graphql'; import DesignNote from './design_note.vue'; import DesignReplyForm from './design_reply_form.vue'; -import { updateStoreAfterAddDiscussionComment } from '../../utils/cache_update'; import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants'; import ToggleRepliesWidget from './toggle_replies_widget.vue'; +import { hasErrors } from '../../utils/cache_update'; +import { ADD_DISCUSSION_COMMENT_ERROR } from '../../utils/error_messages'; export default { components: { @@ -26,6 +27,7 @@ export default { GlLink, ToggleRepliesWidget, TimeAgoTooltip, + GlBadge, }, directives: { GlTooltip: GlTooltipDirective, @@ -62,22 +64,20 @@ export default { activeDiscussion: { query: activeDiscussionQuery, result({ data }) { - const discussionId = data.activeDiscussion.id; if (this.discussion.resolved && !this.resolvedDiscussionsExpanded) { return; } - // We watch any changes to the active discussion from the design pins and scroll to this discussion if it exists - // We don't want scrollIntoView to be triggered from the discussion click itself - if ( - discussionId && - data.activeDiscussion.source === ACTIVE_DISCUSSION_SOURCE_TYPES.pin && - discussionId === this.discussion.notes[0].id - ) { - this.$el.scrollIntoView({ - behavior: 'smooth', - inline: 'start', - }); - } + + this.$nextTick(() => { + // We watch any changes to the active discussion from the design pins and scroll to this discussion if it exists. + // We don't want scrollIntoView to be triggered from the discussion click itself. + if (this.$el && this.shouldScrollToDiscussion(data.activeDiscussion)) { + this.$el.scrollIntoView({ + behavior: 'smooth', + inline: 'start', + }); + } + }); }, }, }, @@ -107,8 +107,8 @@ export default { atVersion: this.designsVersion, }; }, - isDiscussionHighlighted() { - return this.discussion.notes[0].id === this.activeDiscussion.id; + isDiscussionActive() { + return this.discussion.notes.some(({ id }) => id === this.activeDiscussion.id); }, resolveCheckboxText() { return this.discussion.resolved @@ -138,21 +138,10 @@ export default { }, }, methods: { - addDiscussionComment( - store, - { - data: { createNote }, - }, - ) { - updateStoreAfterAddDiscussionComment( - store, - createNote, - getDesignQuery, - this.designVariables, - this.discussion.id, - ); - }, - onDone() { + onDone({ data: { createNote } }) { + if (hasErrors(createNote)) { + createFlash({ message: ADD_DISCUSSION_COMMENT_ERROR }); + } this.discussionComment = ''; this.hideForm(); if (this.shouldChangeResolvedStatus) { @@ -160,14 +149,14 @@ export default { } }, onCreateNoteError(err) { - this.$emit('createNoteError', err); + this.$emit('create-note-error', err); }, hideForm() { this.isFormRendered = false; this.discussionComment = ''; }, showForm() { - this.$emit('openForm', this.discussion.id); + this.$emit('open-form', this.discussion.id); this.isFormRendered = true; }, toggleResolvedStatus() { @@ -179,16 +168,24 @@ export default { }) .then(({ data }) => { if (data.errors?.length > 0) { - this.$emit('resolveDiscussionError', data.errors[0]); + this.$emit('resolve-discussion-error', data.errors[0]); } }) .catch(err => { - this.$emit('resolveDiscussionError', err); + this.$emit('resolve-discussion-error', err); }) .finally(() => { this.isResolving = false; }); }, + shouldScrollToDiscussion(activeDiscussion) { + const ALLOWED_ACTIVE_DISCUSSION_SOURCES = [ + ACTIVE_DISCUSSION_SOURCE_TYPES.pin, + ACTIVE_DISCUSSION_SOURCE_TYPES.url, + ]; + const { source } = activeDiscussion; + return ALLOWED_ACTIVE_DISCUSSION_SOURCES.includes(source) && this.isDiscussionActive; + }, }, createNoteMutation, }; @@ -196,13 +193,12 @@ export default { <template> <div class="design-discussion-wrapper"> - <div - class="badge badge-pill gl-display-flex gl-align-items-center gl-justify-content-center" + <gl-badge + class="gl-display-flex gl-align-items-center gl-justify-content-center gl-cursor-pointer" :class="{ resolved: discussion.resolved }" - type="button" > {{ discussion.index }} - </div> + </gl-badge> <ul class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none" data-qa-selector="design_discussion_content" @@ -211,8 +207,8 @@ export default { :note="firstNote" :markdown-preview-path="markdownPreviewPath" :is-resolving="isResolving" - :class="{ 'gl-bg-blue-50': isDiscussionHighlighted }" - @error="$emit('updateNoteError', $event)" + :class="{ 'gl-bg-blue-50': isDiscussionActive }" + @error="$emit('update-note-error', $event)" > <template v-if="discussion.resolvable" #resolveDiscussion> <button @@ -220,7 +216,6 @@ export default { :class="{ 'is-active': discussion.resolved }" :title="resolveCheckboxText" :aria-label="resolveCheckboxText" - type="button" class="line-resolve-btn note-action-button gl-mr-3" data-testid="resolve-button" @click.stop="toggleResolvedStatus" @@ -255,8 +250,8 @@ export default { :note="note" :markdown-preview-path="markdownPreviewPath" :is-resolving="isResolving" - :class="{ 'gl-bg-blue-50': isDiscussionHighlighted }" - @error="$emit('updateNoteError', $event)" + :class="{ 'gl-bg-blue-50': isDiscussionActive }" + @error="$emit('update-note-error', $event)" /> <li v-show="isReplyPlaceholderVisible" class="reply-wrapper"> <reply-placeholder @@ -272,7 +267,6 @@ export default { :variables="{ input: mutationPayload, }" - :update="addDiscussionComment" @done="onDone" @error="onCreateNoteError" > @@ -280,8 +274,8 @@ export default { v-model="discussionComment" :is-saving="loading" :markdown-preview-path="markdownPreviewPath" - @submitForm="mutate" - @cancelForm="hideForm" + @submit-form="mutate" + @cancel-form="hideForm" > <template v-if="discussion.resolvable" #resolveCheckbox> <label data-testid="resolve-checkbox"> diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue index 172e61920ef..7f4b3b31024 100644 --- a/app/assets/javascripts/design_management/components/design_notes/design_note.vue +++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue @@ -1,12 +1,12 @@ <script> import { ApolloMutation } from 'vue-apollo'; -import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; +import { GlTooltipDirective, GlIcon, GlLink, GlSafeHtmlDirective } from '@gitlab/ui'; import updateNoteMutation from '../../graphql/mutations/update_note.mutation.graphql'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import DesignReplyForm from './design_reply_form.vue'; -import { findNoteId } from '../../utils/design_management_utils'; +import { findNoteId, extractDesignNoteId } from '../../utils/design_management_utils'; import { hasErrors } from '../../utils/cache_update'; export default { @@ -17,9 +17,11 @@ export default { DesignReplyForm, ApolloMutation, GlIcon, + GlLink, }, directives: { GlTooltip: GlTooltipDirective, + SafeHtml: GlSafeHtmlDirective, }, props: { note: { @@ -46,7 +48,7 @@ export default { return findNoteId(this.note.id); }, isNoteLinked() { - return this.$route.hash === `#note_${this.noteAnchorId}`; + return extractDesignNoteId(this.$route.hash) === this.noteAnchorId; }, mutationPayload() { return { @@ -58,11 +60,6 @@ export default { return !this.isEditing && this.note.userPermissions.adminNote; }, }, - mounted() { - if (this.isNoteLinked) { - this.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' }); - } - }, methods: { hideForm() { this.isEditing = false; @@ -87,30 +84,30 @@ export default { :img-alt="author.username" :img-size="40" /> - <div class="d-flex justify-content-between"> + <div class="gl-display-flex gl-justify-content-space-between"> <div> - <a + <gl-link v-once :href="author.webUrl" class="js-user-link" :data-user-id="author.id" :data-username="author.username" > - <span class="note-header-author-name bold">{{ author.name }}</span> - <span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span> + <span class="note-header-author-name gl-font-weight-bold">{{ author.name }}</span> + <span v-if="author.status_tooltip_html" v-safe-html="author.status_tooltip_html"></span> <span class="note-headline-light">@{{ author.username }}</span> - </a> + </gl-link> <span class="note-headline-light note-headline-meta"> <span class="system-note-message"> <slot></slot> </span> - <template v-if="note.createdAt"> - <span class="system-note-separator"></span> - <a class="note-timestamp system-note-separator" :href="`#note_${noteAnchorId}`"> - <time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" /> - </a> - </template> + <gl-link + class="note-timestamp system-note-separator gl-display-block gl-mb-2" + :href="`#note_${noteAnchorId}`" + > + <time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" /> + </gl-link> </span> </div> - <div class="gl-display-flex"> + <div class="gl-display-flex gl-align-items-baseline"> <slot name="resolveDiscussion"></slot> <button v-if="isEditButtonVisible" @@ -126,9 +123,9 @@ export default { </div> <template v-if="!isEditing"> <div + v-safe-html="note.bodyHtml" class="note-text js-note-text md" data-qa-selector="note_content" - v-html="note.bodyHtml" ></div> <slot name="resolvedStatus"></slot> </template> @@ -147,9 +144,9 @@ export default { :is-saving="loading" :markdown-preview-path="markdownPreviewPath" :is-new-comment="false" - class="mt-5" - @submitForm="mutate" - @cancelForm="hideForm" + class="gl-mt-5" + @submit-form="mutate" + @cancel-form="hideForm" /> </apollo-mutation> </timeline-entry-item> 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 969034909f2..3754e1dbbc1 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 @@ -1,5 +1,5 @@ <script> -import { GlDeprecatedButton, GlModal } from '@gitlab/ui'; +import { GlButton, GlModal } from '@gitlab/ui'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import { s__ } from '~/locale'; @@ -7,7 +7,7 @@ export default { name: 'DesignReplyForm', components: { MarkdownField, - GlDeprecatedButton, + GlButton, GlModal, }, props: { @@ -66,13 +66,13 @@ export default { }, methods: { submitForm() { - if (this.hasValue) this.$emit('submitForm'); + if (this.hasValue) this.$emit('submit-form'); }, cancelComment() { if (this.hasValue && this.formText !== this.value) { this.$refs.cancelCommentModal.show(); } else { - this.$emit('cancelForm'); + this.$emit('cancel-form'); } }, focusInput() { @@ -112,20 +112,21 @@ export default { </markdown-field> <slot name="resolveCheckbox"></slot> <div class="note-form-actions gl-display-flex gl-justify-content-space-between"> - <gl-deprecated-button + <gl-button ref="submitButton" :disabled="!hasValue || isSaving" + category="primary" variant="success" type="submit" data-track-event="click_button" data-qa-selector="save_comment_button" - @click="$emit('submitForm')" + @click="$emit('submit-form')" > {{ buttonText }} - </gl-deprecated-button> - <gl-deprecated-button ref="cancelButton" @click="cancelComment">{{ + </gl-button> + <gl-button ref="cancelButton" variant="default" category="primary" @click="cancelComment">{{ __('Cancel') - }}</gl-deprecated-button> + }}</gl-button> </div> <gl-modal ref="cancelCommentModal" @@ -134,7 +135,7 @@ export default { :ok-title="modalSettings.okTitle" :cancel-title="modalSettings.cancelTitle" modal-id="cancel-comment-modal" - @ok="$emit('cancelForm')" + @ok="$emit('cancel-form')" >{{ modalSettings.content }} </gl-modal> </form> diff --git a/app/assets/javascripts/design_management/components/design_overlay.vue b/app/assets/javascripts/design_management/components/design_overlay.vue index 926e7c74802..5c4a3ab5f94 100644 --- a/app/assets/javascripts/design_management/components/design_overlay.vue +++ b/app/assets/javascripts/design_management/components/design_overlay.vue @@ -1,4 +1,5 @@ <script> +import { __ } from '~/locale'; import activeDiscussionQuery from '../graphql/queries/active_discussion.query.graphql'; import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql'; import DesignNotePin from './design_note_pin.vue'; @@ -236,18 +237,26 @@ export default { }); }, isNoteInactive(note) { - return this.activeDiscussion.id && this.activeDiscussion.id !== note.id; + const discussionNotes = note.discussion.notes.nodes || []; + + return ( + this.activeDiscussion.id && + !discussionNotes.some(({ id }) => id === this.activeDiscussion.id) + ); }, designPinClass(note) { return { inactive: this.isNoteInactive(note), resolved: note.resolved }; }, }, + i18n: { + newCommentButtonLabel: __('Add comment to design'), + }, }; </script> <template> <div - class="position-absolute image-diff-overlay frame" + class="gl-absolute gl-top-0 gl-left-0 frame" :style="overlayStyle" @mousemove="onOverlayMousemove" @mouseleave="onNoteMouseup" @@ -255,26 +264,28 @@ export default { <button v-show="!disableCommenting" type="button" - class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button" + role="button" + :aria-label="$options.i18n.newCommentButtonLabel" + class="gl-absolute gl-w-full gl-h-full gl-p-0 gl-top-0 gl-left-0 gl-outline-0! btn-transparent design-detail-overlay-add-comment" data-qa-selector="design_image_button" @mouseup="onAddCommentMouseup" ></button> - <template v-for="note in notes"> - <design-note-pin - v-if="resolvedDiscussionsExpanded || !note.resolved" - :key="note.id" - :label="note.index" - :repositioning="isMovingNote(note.id)" - :position=" - isMovingNote(note.id) && movingNoteNewPosition - ? getNotePositionStyle(movingNoteNewPosition) - : getNotePositionStyle(note.position) - " - :class="designPinClass(note)" - @mousedown.stop="onNoteMousedown($event, note)" - @mouseup.stop="onNoteMouseup(note)" - /> - </template> + + <design-note-pin + v-for="note in notes" + v-if="resolvedDiscussionsExpanded || !note.resolved" + :key="note.id" + :label="note.index" + :repositioning="isMovingNote(note.id)" + :position=" + isMovingNote(note.id) && movingNoteNewPosition + ? getNotePositionStyle(movingNoteNewPosition) + : getNotePositionStyle(note.position) + " + :class="designPinClass(note)" + @mousedown.stop="onNoteMousedown($event, note)" + @mouseup.stop="onNoteMouseup(note)" + /> <design-note-pin v-if="currentCommentForm" diff --git a/app/assets/javascripts/design_management/components/design_sidebar.vue b/app/assets/javascripts/design_management/components/design_sidebar.vue index e5a3590877e..df425e3b96d 100644 --- a/app/assets/javascripts/design_management/components/design_sidebar.vue +++ b/app/assets/javascripts/design_management/components/design_sidebar.vue @@ -8,6 +8,8 @@ import { extractDiscussions, extractParticipants } from '../utils/design_managem import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants'; import DesignDiscussion from './design_notes/design_discussion.vue'; import Participants from '~/sidebar/components/participants/participants.vue'; +import DesignTodoButton from './design_todo_button.vue'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { components: { @@ -16,7 +18,9 @@ export default { GlCollapse, GlButton, GlPopover, + DesignTodoButton, }, + mixins: [glFeatureFlagsMixin()], props: { design: { type: Object, @@ -37,6 +41,14 @@ export default { discussionWithOpenForm: '', }; }, + inject: { + projectPath: { + default: '', + }, + issueIid: { + default: '', + }, + }, computed: { discussions() { return extractDiscussions(this.design.discussions); @@ -59,6 +71,26 @@ export default { resolvedCommentsToggleIcon() { return this.resolvedDiscussionsExpanded ? 'chevron-down' : 'chevron-right'; }, + showTodoButton() { + return this.glFeatures.designManagementTodoButton; + }, + sidebarWrapperClass() { + return { + 'gl-pt-0': this.showTodoButton, + }; + }, + }, + watch: { + isResolvedCommentsPopoverHidden(newVal) { + if (!newVal) { + this.$refs.resolvedComments.scrollIntoView(); + } + }, + }, + mounted() { + if (!this.isResolvedCommentsPopoverHidden && this.$refs.resolvedComments) { + this.$refs.resolvedComments.$el.scrollIntoView(); + } }, methods: { handleSidebarClick() { @@ -89,7 +121,14 @@ export default { </script> <template> - <div class="image-notes" @click="handleSidebarClick"> + <div class="image-notes" :class="sidebarWrapperClass" @click="handleSidebarClick"> + <div + v-if="showTodoButton" + class="gl-py-4 gl-mb-4 gl-display-flex gl-justify-content-space-between gl-align-items-center gl-border-b-1 gl-border-b-solid gl-border-b-gray-100" + > + <span>{{ __('To-Do') }}</span> + <design-todo-button :design="design" @error="$emit('todoError', $event)" /> + </div> <h2 class="gl-font-weight-bold gl-mt-0"> {{ issue.title }} </h2> @@ -120,15 +159,16 @@ export default { :resolved-discussions-expanded="resolvedDiscussionsExpanded" :discussion-with-open-form="discussionWithOpenForm" data-testid="unresolved-discussion" - @createNoteError="$emit('onDesignDiscussionError', $event)" - @updateNoteError="$emit('updateNoteError', $event)" - @resolveDiscussionError="$emit('resolveDiscussionError', $event)" + @create-note-error="$emit('onDesignDiscussionError', $event)" + @update-note-error="$emit('updateNoteError', $event)" + @resolve-discussion-error="$emit('resolveDiscussionError', $event)" @click.native.stop="updateActiveDiscussion(discussion.notes[0].id)" - @openForm="updateDiscussionWithOpenForm" + @open-form="updateDiscussionWithOpenForm" /> <template v-if="resolvedDiscussions.length > 0"> <gl-button id="resolved-comments" + ref="resolvedComments" data-testid="resolved-comments" :icon="resolvedCommentsToggleIcon" variant="link" @@ -151,9 +191,12 @@ export default { ) }} </p> - <a href="#" rel="noopener noreferrer" target="_blank">{{ - s__('DesignManagement|Learn more about resolving comments') - }}</a> + <a + href="https://docs.gitlab.com/ee/user/project/issues/design_management.html#resolve-design-threads" + rel="noopener noreferrer" + target="_blank" + >{{ s__('DesignManagement|Learn more about resolving comments') }}</a + > </gl-popover> <gl-collapse :visible="resolvedDiscussionsExpanded" class="gl-mt-3"> <design-discussion diff --git a/app/assets/javascripts/design_management/components/design_todo_button.vue b/app/assets/javascripts/design_management/components/design_todo_button.vue new file mode 100644 index 00000000000..aff4f348d15 --- /dev/null +++ b/app/assets/javascripts/design_management/components/design_todo_button.vue @@ -0,0 +1,168 @@ +<script> +import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql'; +import getDesignQuery from '../graphql/queries/get_design.query.graphql'; +import createDesignTodoMutation from '../graphql/mutations/create_design_todo.mutation.graphql'; +import TodoButton from '~/vue_shared/components/todo_button.vue'; +import allVersionsMixin from '../mixins/all_versions'; +import { updateStoreAfterDeleteDesignTodo } from '../utils/cache_update'; +import { findIssueId, findDesignId } from '../utils/design_management_utils'; +import { CREATE_DESIGN_TODO_ERROR, DELETE_DESIGN_TODO_ERROR } from '../utils/error_messages'; + +export default { + components: { + TodoButton, + }, + mixins: [allVersionsMixin], + props: { + design: { + type: Object, + required: true, + }, + }, + inject: { + projectPath: { + default: '', + }, + issueIid: { + default: '', + }, + }, + data() { + return { + todoLoading: false, + }; + }, + computed: { + designVariables() { + return { + fullPath: this.projectPath, + iid: this.issueIid, + filenames: [this.$route.params.id], + atVersion: this.designsVersion, + }; + }, + designTodoVariables() { + return { + projectPath: this.projectPath, + issueId: findIssueId(this.design.issue.id), + designId: findDesignId(this.design.id), + issueIid: this.issueIid, + filenames: [this.$route.params.id], + atVersion: this.designsVersion, + }; + }, + pendingTodo() { + // TODO data structure pending BE MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40555#note_405732940 + return this.design.currentUserTodos?.nodes[0]; + }, + hasPendingTodo() { + return Boolean(this.pendingTodo); + }, + }, + methods: { + updateGlobalTodoCount(additionalTodoCount) { + const currentCount = parseInt(document.querySelector('.js-todos-count').innerText, 10); + const todoToggleEvent = new CustomEvent('todo:toggle', { + detail: { + count: Math.max(currentCount + additionalTodoCount, 0), + }, + }); + + document.dispatchEvent(todoToggleEvent); + }, + incrementGlobalTodoCount() { + this.updateGlobalTodoCount(1); + }, + decrementGlobalTodoCount() { + this.updateGlobalTodoCount(-1); + }, + createTodo() { + this.todoLoading = true; + return this.$apollo + .mutate({ + mutation: createDesignTodoMutation, + variables: this.designTodoVariables, + update: (store, { data: { createDesignTodo } }) => { + // because this is a @client mutation, + // we control what is in errors, and therefore + // we are certain that there is at most 1 item in the array + const createDesignTodoError = (createDesignTodo.errors || [])[0]; + if (createDesignTodoError) { + this.$emit('error', Error(createDesignTodoError.message)); + } + }, + }) + .then(() => { + this.incrementGlobalTodoCount(); + }) + .catch(err => { + this.$emit('error', Error(CREATE_DESIGN_TODO_ERROR)); + throw err; + }) + .finally(() => { + this.todoLoading = false; + }); + }, + deleteTodo() { + if (!this.hasPendingTodo) return Promise.reject(); + + const { id } = this.pendingTodo; + const { designVariables } = this; + + this.todoLoading = true; + return this.$apollo + .mutate({ + mutation: todoMarkDoneMutation, + variables: { + id, + }, + update( + store, + { + data: { todoMarkDone }, + }, + ) { + const todoMarkDoneFirstError = (todoMarkDone.errors || [])[0]; + if (todoMarkDoneFirstError) { + this.$emit('error', Error(todoMarkDoneFirstError)); + } else { + updateStoreAfterDeleteDesignTodo( + store, + todoMarkDone, + getDesignQuery, + designVariables, + ); + } + }, + }) + .then(() => { + this.decrementGlobalTodoCount(); + }) + .catch(err => { + this.$emit('error', Error(DELETE_DESIGN_TODO_ERROR)); + throw err; + }) + .finally(() => { + this.todoLoading = false; + }); + }, + toggleTodo() { + if (this.hasPendingTodo) { + return this.deleteTodo(); + } + + return this.createTodo(); + }, + }, +}; +</script> + +<template> + <todo-button + issuable-type="design" + :issuable-id="design.iid" + :is-todo="hasPendingTodo" + :loading="todoLoading" + @click.stop.prevent="toggleTodo" + /> +</template> diff --git a/app/assets/javascripts/design_management/components/list/item.vue b/app/assets/javascripts/design_management/components/list/item.vue index 292b6e09055..36ea812d92e 100644 --- a/app/assets/javascripts/design_management/components/list/item.vue +++ b/app/assets/javascripts/design_management/components/list/item.vue @@ -1,6 +1,5 @@ <script> import { GlLoadingIcon, GlIcon, GlIntersectionObserver } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; import Timeago from '~/vue_shared/components/time_ago_tooltip.vue'; import { n__, __ } from '~/locale'; import { DESIGN_ROUTE_NAME } from '../../router/constants'; @@ -10,7 +9,6 @@ export default { GlLoadingIcon, GlIntersectionObserver, GlIcon, - Icon, Timeago, }, props: { @@ -127,12 +125,14 @@ export default { params: { id: filename }, query: $route.query, }" - class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new" + class="card gl-cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new" > - <div class="card-body p-0 d-flex-center overflow-hidden position-relative"> - <div v-if="icon.name" data-testid="designEvent" class="design-event position-absolute"> + <div + class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative" + > + <div v-if="icon.name" data-testid="designEvent" class="design-event gl-absolute"> <span :title="icon.tooltip" :aria-label="icon.tooltip"> - <icon :name="icon.name" :size="18" :class="icon.classes" /> + <gl-icon :name="icon.name" :size="18" :class="icon.classes" /> </span> </div> <gl-intersection-observer @appear="onAppear"> @@ -147,25 +147,28 @@ export default { v-show="showImage" :src="imageLink" :alt="filename" - class="block mx-auto mw-100 mh-100 design-img" + class="gl-display-block gl-mx-auto gl-max-w-full mh-100 design-img" data-qa-selector="design_image" @load="onImageLoad" @error="onImageError" /> </gl-intersection-observer> </div> - <div class="card-footer d-flex w-100"> - <div class="d-flex flex-column str-truncated-100"> - <span class="bold str-truncated-100" data-qa-selector="design_file_name">{{ + <div class="card-footer gl-display-flex gl-w-full"> + <div class="gl-display-flex gl-flex-direction-column str-truncated-100"> + <span class="gl-font-weight-bold str-truncated-100" data-qa-selector="design_file_name">{{ filename }}</span> <span v-if="updatedAt" class="str-truncated-100"> {{ __('Updated') }} <timeago :time="updatedAt" tooltip-placement="bottom" /> </span> </div> - <div v-if="notesCount" class="ml-auto d-flex align-items-center text-secondary"> - <icon name="comments" class="ml-1" /> - <span :aria-label="notesLabel" class="ml-1"> + <div + v-if="notesCount" + class="gl-ml-auto gl-display-flex gl-align-items-center gl-text-gray-500" + > + <gl-icon name="comments" class="gl-ml-2" /> + <span :aria-label="notesLabel" class="gl-ml-2"> {{ notesCount }} </span> </div> 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 a03982cb91b..4a1be7b720a 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 @@ -1,13 +1,13 @@ <script> -import { GlNewDropdown, GlNewDropdownItem, GlSprintf } from '@gitlab/ui'; +import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import allVersionsMixin from '../../mixins/all_versions'; import { findVersionId } from '../../utils/design_management_utils'; export default { components: { - GlNewDropdown, - GlNewDropdownItem, + GlDropdown, + GlDropdownItem, GlSprintf, }, mixins: [allVersionsMixin], @@ -63,8 +63,8 @@ export default { </script> <template> - <gl-new-dropdown :text="dropdownText" size="small"> - <gl-new-dropdown-item + <gl-dropdown :text="dropdownText" size="small"> + <gl-dropdown-item v-for="(version, index) in allVersions" :key="version.id" :is-check-item="true" @@ -76,6 +76,6 @@ export default { {{ allVersions.length - index }} </template> </gl-sprintf> - </gl-new-dropdown-item> - </gl-new-dropdown> + </gl-dropdown-item> + </gl-dropdown> </template> diff --git a/app/assets/javascripts/design_management/constants.js b/app/assets/javascripts/design_management/constants.js index 21ff361a277..63a92ef5ec0 100644 --- a/app/assets/javascripts/design_management/constants.js +++ b/app/assets/javascripts/design_management/constants.js @@ -11,6 +11,7 @@ export const VALID_DATA_TRANSFER_TYPE = 'Files'; export const ACTIVE_DISCUSSION_SOURCE_TYPES = { pin: 'pin', discussion: 'discussion', + url: 'url', }; export const DESIGN_DETAIL_LAYOUT_CLASSLIST = ['design-detail-layout', 'overflow-hidden', 'm-0']; diff --git a/app/assets/javascripts/design_management/graphql.js b/app/assets/javascripts/design_management/graphql.js index fae337aa75b..d1fe977b969 100644 --- a/app/assets/javascripts/design_management/graphql.js +++ b/app/assets/javascripts/design_management/graphql.js @@ -1,24 +1,70 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import { uniqueId } from 'lodash'; +import produce from 'immer'; import { defaultDataIdFromObject } from 'apollo-cache-inmemory'; +import axios from '~/lib/utils/axios_utils'; import createDefaultClient from '~/lib/graphql'; import activeDiscussionQuery from './graphql/queries/active_discussion.query.graphql'; +import getDesignQuery from './graphql/queries/get_design.query.graphql'; import typeDefs from './graphql/typedefs.graphql'; +import { extractTodoIdFromDeletePath, createPendingTodo } from './utils/design_management_utils'; +import { CREATE_DESIGN_TODO_EXISTS_ERROR } from './utils/error_messages'; +import { addPendingTodoToStore } from './utils/cache_update'; Vue.use(VueApollo); const resolvers = { Mutation: { updateActiveDiscussion: (_, { id = null, source }, { cache }) => { - const data = cache.readQuery({ query: activeDiscussionQuery }); - data.activeDiscussion = { - __typename: 'ActiveDiscussion', - id, - source, - }; + const sourceData = cache.readQuery({ query: activeDiscussionQuery }); + + const data = produce(sourceData, draftData => { + // eslint-disable-next-line no-param-reassign + draftData.activeDiscussion = { + __typename: 'ActiveDiscussion', + id, + source, + }; + }); + cache.writeQuery({ query: activeDiscussionQuery, data }); }, + createDesignTodo: ( + _, + { projectPath, issueId, designId, issueIid, filenames, atVersion }, + { cache }, + ) => { + return axios + .post(`/${projectPath}/todos`, { + issue_id: issueId, + issuable_id: designId, + issuable_type: 'design', + }) + .then(({ data }) => { + const { delete_path } = data; + const todoId = extractTodoIdFromDeletePath(delete_path); + if (!todoId) { + return { + errors: [ + { + message: CREATE_DESIGN_TODO_EXISTS_ERROR, + }, + ], + }; + } + + const pendingTodo = createPendingTodo(todoId); + addPendingTodoToStore(cache, pendingTodo, getDesignQuery, { + fullPath: projectPath, + iid: issueIid, + filenames, + atVersion, + }); + + return pendingTodo; + }); + }, }, }; @@ -37,6 +83,7 @@ const defaultClient = createDefaultClient( }, }, typeDefs, + assumeImmutableResults: true, }, ); diff --git a/app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql index bc3132f9b42..9bd70e7e886 100644 --- a/app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql +++ b/app/assets/javascripts/design_management/graphql/fragments/design_list.fragment.graphql @@ -5,4 +5,9 @@ fragment DesignListItem on Design { notesCount image imageV432x230 + currentUserTodos(state: pending) { + nodes { + id + } + } } diff --git a/app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql index 26edd2c0be1..28224671326 100644 --- a/app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql +++ b/app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql @@ -25,5 +25,10 @@ fragment DesignNote on Note { } discussion { id + notes { + nodes { + id + } + } } } diff --git a/app/assets/javascripts/design_management/graphql/mutations/create_design_todo.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/create_design_todo.mutation.graphql new file mode 100644 index 00000000000..0c989b2fdde --- /dev/null +++ b/app/assets/javascripts/design_management/graphql/mutations/create_design_todo.mutation.graphql @@ -0,0 +1,17 @@ +mutation createDesignTodo( + $projectPath: String! + $issueId: String! + $designId: String! + $issueIid: String! + $filenames: [String]! + $atVersion: String +) { + createDesignTodo( + projectPath: $projectPath + issueId: $issueId + designId: $designId + issueIid: $issueIid + filenames: $filenames + atVersion: $atVersion + ) @client +} diff --git a/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql b/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql index ab987dda525..96869a404b1 100644 --- a/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql +++ b/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql @@ -10,6 +10,7 @@ query getDesign($fullPath: ID!, $iid: String!, $atVersion: ID, $filenames: [Stri nodes { ...DesignItem issue { + id title webPath webUrl diff --git a/app/assets/javascripts/design_management/index.js b/app/assets/javascripts/design_management/index.js index 20c9cacf83f..1a87dd38137 100644 --- a/app/assets/javascripts/design_management/index.js +++ b/app/assets/javascripts/design_management/index.js @@ -4,7 +4,7 @@ import App from './components/app.vue'; import apolloProvider from './graphql'; export default () => { - const el = document.querySelector('.js-design-management-new'); + const el = document.querySelector('.js-design-management'); const { issueIid, projectPath, issuePath } = el.dataset; const router = createRouter(issuePath); diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue index 17b72e73127..c6225c516e2 100644 --- a/app/assets/javascripts/design_management/pages/design/index.vue +++ b/app/assets/javascripts/design_management/pages/design/index.vue @@ -19,6 +19,8 @@ import { extractDiscussions, extractDesign, updateImageDiffNoteOptimisticResponse, + toDiffNoteGid, + extractDesignNoteId, } from '../../utils/design_management_utils'; import { updateStoreAfterAddImageDiffNote, @@ -31,6 +33,7 @@ import { DESIGN_NOT_FOUND_ERROR, DESIGN_VERSION_NOT_EXIST_ERROR, UPDATE_NOTE_ERROR, + TOGGLE_TODO_ERROR, designDeletionError, } from '../../utils/error_messages'; import { trackDesignDetailView } from '../../utils/tracking'; @@ -145,8 +148,11 @@ export default { mounted() { Mousetrap.bind('esc', this.closeDesign); this.trackEvent(); - // We need to reset the active discussion when opening a new design - this.updateActiveDiscussion(); + + // Set active discussion immediately. + // This will ensure that, if a note is specified in the URL hash, + // the browser will scroll to, and highlight, the note in the UI + this.updateActiveDiscussionFromUrl(); }, beforeDestroy() { Mousetrap.unbind('esc', this.closeDesign); @@ -221,7 +227,7 @@ export default { }, onError(message, e) { this.errorMessage = message; - throw e; + if (e) throw e; }, onCreateImageDiffNoteError(e) { this.onError(ADD_IMAGE_DIFF_NOTE_ERROR, e); @@ -241,6 +247,9 @@ export default { onResolveDiscussionError(e) { this.onError(UPDATE_IMAGE_DIFF_NOTE_ERROR, e); }, + onTodoError(e) { + this.onError(e?.message || TOGGLE_TODO_ERROR, e); + }, openCommentForm(annotationCoordinates) { this.annotationCoordinates = annotationCoordinates; if (this.$refs.newDiscussionForm) { @@ -266,15 +275,20 @@ export default { this.isLatestVersion, ); }, - updateActiveDiscussion(id) { + updateActiveDiscussion(id, source = ACTIVE_DISCUSSION_SOURCE_TYPES.discussion) { this.$apollo.mutate({ mutation: updateActiveDiscussionMutation, variables: { id, - source: ACTIVE_DISCUSSION_SOURCE_TYPES.discussion, + source, }, }); }, + updateActiveDiscussionFromUrl() { + const noteId = extractDesignNoteId(this.$route.hash); + const diffNoteGid = noteId ? toDiffNoteGid(noteId) : undefined; + return this.updateActiveDiscussion(diffNoteGid, ACTIVE_DISCUSSION_SOURCE_TYPES.url); + }, toggleResolvedComments() { this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded; }, @@ -339,6 +353,7 @@ export default { @updateNoteError="onUpdateNoteError" @resolveDiscussionError="onResolveDiscussionError" @toggleResolvedComments="toggleResolvedComments" + @todoError="onTodoError" > <template #replyForm> <apollo-mutation @@ -357,8 +372,8 @@ export default { v-model="comment" :is-saving="loading" :markdown-preview-path="markdownPreviewPath" - @submitForm="mutate" - @cancelForm="closeCommentForm" + @submit-form="mutate" + @cancel-form="closeCommentForm" /> </apollo-mutation ></template> </design-sidebar> diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue index cd68e9d6c5b..6c4c8c75054 100644 --- a/app/assets/javascripts/design_management/pages/index.vue +++ b/app/assets/javascripts/design_management/pages/index.vue @@ -281,13 +281,8 @@ export default { .mutate({ mutation: moveDesignMutation, variables: this.designMoveVariables(newIndex, element), - update: (store, { data: { designManagementMove } }) => { - return updateDesignsOnStoreAfterReorder( - store, - designManagementMove, - this.projectQueryBody, - ); - }, + update: (store, { data: { designManagementMove } }) => + updateDesignsOnStoreAfterReorder(store, designManagementMove, this.projectQueryBody), optimisticResponse: moveDesignOptimisticResponse(this.reorderedDesigns), }) .catch(() => { @@ -327,7 +322,7 @@ export default { v-if="isLatestVersion" variant="link" size="small" - class="gl-mr-3 js-select-all" + class="gl-mr-4 js-select-all" @click="toggleDesignsSelection" >{{ selectAllButtonText }} </gl-button> diff --git a/app/assets/javascripts/design_management/utils/cache_update.js b/app/assets/javascripts/design_management/utils/cache_update.js index b79df9d01d5..ff41136fd54 100644 --- a/app/assets/javascripts/design_management/utils/cache_update.js +++ b/app/assets/javascripts/design_management/utils/cache_update.js @@ -1,22 +1,27 @@ /* eslint-disable @gitlab/require-i18n-strings */ import { groupBy } from 'lodash'; +import produce from 'immer'; import { deprecatedCreateFlash as createFlash } from '~/flash'; -import { extractCurrentDiscussion, extractDesign } from './design_management_utils'; +import { extractCurrentDiscussion, extractDesign, extractDesigns } from './design_management_utils'; import { ADD_IMAGE_DIFF_NOTE_ERROR, UPDATE_IMAGE_DIFF_NOTE_ERROR, - ADD_DISCUSSION_COMMENT_ERROR, + DELETE_DESIGN_TODO_ERROR, designDeletionError, } from './error_messages'; +const designsOf = data => data.project.issue.designCollection.designs; + const deleteDesignsFromStore = (store, query, selectedDesigns) => { - const data = store.readQuery(query); + const sourceData = store.readQuery(query); - const changedDesigns = data.project.issue.designCollection.designs.nodes.filter( - node => !selectedDesigns.includes(node.filename), - ); - data.project.issue.designCollection.designs.nodes = [...changedDesigns]; + const data = produce(sourceData, draftData => { + const changedDesigns = designsOf(sourceData).nodes.filter( + design => !selectedDesigns.includes(design.filename), + ); + designsOf(draftData).nodes = [...changedDesigns]; + }); store.writeQuery({ ...query, @@ -33,13 +38,15 @@ const deleteDesignsFromStore = (store, query, selectedDesigns) => { */ const addNewVersionToStore = (store, query, version) => { if (!version) return; + const sourceData = store.readQuery(query); - const data = store.readQuery(query); - - data.project.issue.designCollection.versions.nodes = [ - version, - ...data.project.issue.designCollection.versions.nodes, - ]; + const data = produce(sourceData, draftData => { + // eslint-disable-next-line no-param-reassign + draftData.project.issue.designCollection.versions.nodes = [ + version, + ...draftData.project.issue.designCollection.versions.nodes, + ]; + }); store.writeQuery({ ...query, @@ -47,47 +54,12 @@ const addNewVersionToStore = (store, query, version) => { }); }; -const addDiscussionCommentToStore = (store, createNote, query, queryVariables, discussionId) => { - const data = store.readQuery({ - query, - variables: queryVariables, - }); - - const design = extractDesign(data); - const currentDiscussion = extractCurrentDiscussion(design.discussions, discussionId); - currentDiscussion.notes.nodes = [...currentDiscussion.notes.nodes, createNote.note]; - - design.notesCount += 1; - if ( - !design.issue.participants.nodes.some( - participant => participant.username === createNote.note.author.username, - ) - ) { - design.issue.participants.nodes = [ - ...design.issue.participants.nodes, - { - __typename: 'User', - ...createNote.note.author, - }, - ]; - } - store.writeQuery({ - query, - variables: queryVariables, - data: { - ...data, - design: { - ...design, - }, - }, - }); -}; - const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) => { - const data = store.readQuery({ + const sourceData = store.readQuery({ query, variables, }); + const newDiscussion = { __typename: 'Discussion', id: createImageDiffNote.note.discussion.id, @@ -101,100 +73,100 @@ const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) = nodes: [createImageDiffNote.note], }, }; - const design = extractDesign(data); - const notesCount = design.notesCount + 1; - design.discussions.nodes = [...design.discussions.nodes, newDiscussion]; - if ( - !design.issue.participants.nodes.some( - participant => participant.username === createImageDiffNote.note.author.username, - ) - ) { - design.issue.participants.nodes = [ - ...design.issue.participants.nodes, - { - __typename: 'User', - ...createImageDiffNote.note.author, - }, - ]; - } + + 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, + ) + ) { + design.issue.participants.nodes = [ + ...design.issue.participants.nodes, + { + __typename: 'User', + ...createImageDiffNote.note.author, + }, + ]; + } + }); + store.writeQuery({ query, variables, - data: { - ...data, - design: { - ...design, - notesCount, - }, - }, + data, }); }; const updateImageDiffNoteInStore = (store, updateImageDiffNote, query, variables) => { - const data = store.readQuery({ + const sourceData = store.readQuery({ query, variables, }); - const design = extractDesign(data); - const discussion = extractCurrentDiscussion( - design.discussions, - updateImageDiffNote.note.discussion.id, - ); - - discussion.notes = { - ...discussion.notes, - nodes: [updateImageDiffNote.note, ...discussion.notes.nodes.slice(1)], - }; + const data = produce(sourceData, draftData => { + const design = extractDesign(draftData); + const discussion = extractCurrentDiscussion( + design.discussions, + updateImageDiffNote.note.discussion.id, + ); + + discussion.notes = { + ...discussion.notes, + nodes: [updateImageDiffNote.note, ...discussion.notes.nodes.slice(1)], + }; + }); store.writeQuery({ query, variables, - data: { - ...data, - design, - }, + data, }); }; const addNewDesignToStore = (store, designManagementUpload, query) => { - const data = store.readQuery(query); + const sourceData = store.readQuery(query); - const currentDesigns = data.project.issue.designCollection.designs.nodes; - const existingDesigns = groupBy(currentDesigns, 'filename'); - const newDesigns = currentDesigns.concat( - designManagementUpload.designs.filter(d => !existingDesigns[d.filename]), - ); + const data = produce(sourceData, draftData => { + const currentDesigns = extractDesigns(draftData); + const existingDesigns = groupBy(currentDesigns, 'filename'); + const newDesigns = currentDesigns.concat( + designManagementUpload.designs.filter(d => !existingDesigns[d.filename]), + ); - let newVersionNode; - const findNewVersions = designManagementUpload.designs.find(design => design.versions); + let newVersionNode; + const findNewVersions = designManagementUpload.designs.find(design => design.versions); - if (findNewVersions) { - const findNewVersionsNodes = findNewVersions.versions.nodes; + if (findNewVersions) { + const findNewVersionsNodes = findNewVersions.versions.nodes; - if (findNewVersionsNodes && findNewVersionsNodes.length) { - newVersionNode = [findNewVersionsNodes[0]]; + if (findNewVersionsNodes && findNewVersionsNodes.length) { + newVersionNode = [findNewVersionsNodes[0]]; + } } - } - - const newVersions = [ - ...(newVersionNode || []), - ...data.project.issue.designCollection.versions.nodes, - ]; - const updatedDesigns = { - __typename: 'DesignCollection', - designs: { - __typename: 'DesignConnection', - nodes: newDesigns, - }, - versions: { - __typename: 'DesignVersionConnection', - nodes: newVersions, - }, - }; + const newVersions = [ + ...(newVersionNode || []), + ...draftData.project.issue.designCollection.versions.nodes, + ]; - data.project.issue.designCollection = updatedDesigns; + const updatedDesigns = { + __typename: 'DesignCollection', + designs: { + __typename: 'DesignConnection', + nodes: newDesigns, + }, + versions: { + __typename: 'DesignVersionConnection', + nodes: newVersions, + }, + }; + // eslint-disable-next-line no-param-reassign + draftData.project.issue.designCollection = updatedDesigns; + }); store.writeQuery({ ...query, @@ -203,14 +175,63 @@ const addNewDesignToStore = (store, designManagementUpload, query) => { }; const moveDesignInStore = (store, designManagementMove, query) => { - const data = store.readQuery(query); - data.project.issue.designCollection.designs = designManagementMove.designCollection.designs; + const sourceData = store.readQuery(query); + + const data = produce(sourceData, draftData => { + // eslint-disable-next-line no-param-reassign + draftData.project.issue.designCollection.designs = + designManagementMove.designCollection.designs; + }); + store.writeQuery({ ...query, data, }); }; +export const addPendingTodoToStore = (store, pendingTodo, query, queryVariables) => { + const sourceData = store.readQuery({ + query, + variables: queryVariables, + }); + + const data = produce(sourceData, draftData => { + const design = extractDesign(draftData); + const existingTodos = design.currentUserTodos?.nodes || []; + const newTodoNodes = [...existingTodos, { ...pendingTodo, __typename: 'Todo' }]; + + if (!design.currentUserTodos) { + design.currentUserTodos = { + __typename: 'TodoConnection', + nodes: newTodoNodes, + }; + } else { + design.currentUserTodos.nodes = newTodoNodes; + } + }); + + store.writeQuery({ query, variables: queryVariables, data }); +}; + +export const deletePendingTodoFromStore = (store, todoMarkDone, query, queryVariables) => { + const sourceData = store.readQuery({ + query, + variables: queryVariables, + }); + + const { + todo: { id: todoId }, + } = todoMarkDone; + const data = produce(sourceData, draftData => { + const design = extractDesign(draftData); + const existingTodos = design.currentUserTodos?.nodes || []; + + design.currentUserTodos.nodes = existingTodos.filter(({ id }) => id !== todoId); + }); + + store.writeQuery({ query, variables: queryVariables, data }); +}; + const onError = (data, message) => { createFlash(message); throw new Error(data.errors); @@ -235,20 +256,6 @@ export const updateStoreAfterDesignsDelete = (store, data, query, designs) => { } }; -export const updateStoreAfterAddDiscussionComment = ( - store, - data, - query, - queryVariables, - discussionId, -) => { - if (hasErrors(data)) { - onError(data, ADD_DISCUSSION_COMMENT_ERROR); - } else { - addDiscussionCommentToStore(store, data, query, queryVariables, discussionId); - } -}; - export const updateStoreAfterAddImageDiffNote = (store, data, query, queryVariables) => { if (hasErrors(data)) { onError(data, ADD_IMAGE_DIFF_NOTE_ERROR); @@ -280,3 +287,11 @@ export const updateDesignsOnStoreAfterReorder = (store, data, query) => { moveDesignInStore(store, data, query); } }; + +export const updateStoreAfterDeleteDesignTodo = (store, data, query, queryVariables) => { + if (hasErrors(data)) { + onError(data, DELETE_DESIGN_TODO_ERROR); + } else { + deletePendingTodoFromStore(store, data, query, queryVariables); + } +}; 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 da8f89ff960..93e4d6060c3 100644 --- a/app/assets/javascripts/design_management/utils/design_management_utils.js +++ b/app/assets/javascripts/design_management/utils/design_management_utils.js @@ -30,10 +30,25 @@ export const findVersionId = id => (id.match('::Version/(.+$)') || [])[1]; export const findNoteId = id => (id.match('DiffNote/(.+$)') || [])[1]; +export const findIssueId = id => (id.match('Issue/(.+$)') || [])[1]; + +export const findDesignId = id => (id.match('Design/(.+$)') || [])[1]; + export const extractDesigns = data => data.project.issue.designCollection.designs.nodes; export const extractDesign = data => (extractDesigns(data) || [])[0]; +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 => { + const [, noteId] = urlHash.match('#note_([0-9]+$)') || []; + return noteId || null; +}; + /** * Generates optimistic response for a design upload mutation * @param {Array<File>} files @@ -135,3 +150,22 @@ const normalizeAuthor = author => ({ export const extractParticipants = users => users.map(node => normalizeAuthor(node)); export const getPageLayoutElement = () => document.querySelector('.layout-page'); + +/** + * Extract the ID of the To-Do for a given 'delete' path + * Example of todoDeletePath: /delete/1234 + * @param {String} todoDeletePath delete_path from REST API response + */ +export const extractTodoIdFromDeletePath = todoDeletePath => + (todoDeletePath.match('todos/([0-9]+$)') || [])[1]; + +const createTodoGid = todoId => { + return `gid://gitlab/Todo/${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 c815b11737d..bd21d711462 100644 --- a/app/assets/javascripts/design_management/utils/error_messages.js +++ b/app/assets/javascripts/design_management/utils/error_messages.js @@ -44,6 +44,14 @@ export const MOVE_DESIGN_ERROR = __( 'Something went wrong when reordering designs. Please try again', ); +export const CREATE_DESIGN_TODO_ERROR = __('Failed to create To-Do for the design.'); + +export const CREATE_DESIGN_TODO_EXISTS_ERROR = __('There is already a To-Do for this design.'); + +export const DELETE_DESIGN_TODO_ERROR = __('Failed to remove To-Do for the design.'); + +export const TOGGLE_TODO_ERROR = __('Failed to toggle To-Do for the design.'); + const MAX_SKIPPED_FILES_LISTINGS = 5; const oneDesignSkippedMessage = filename => diff --git a/app/assets/javascripts/design_management/utils/tracking.js b/app/assets/javascripts/design_management/utils/tracking.js index b3ecc1453a6..49fa306914c 100644 --- a/app/assets/javascripts/design_management/utils/tracking.js +++ b/app/assets/javascripts/design_management/utils/tracking.js @@ -5,7 +5,6 @@ const DESIGN_TRACKING_CONTEXT_SCHEMA = 'iglu:com.gitlab/design_management_contex const DESIGN_TRACKING_PAGE_NAME = 'projects:issues:design'; const DESIGN_TRACKING_EVENT_NAME = 'view_design'; -// eslint-disable-next-line import/prefer-default-export export function trackDesignDetailView( referer = '', owner = '', diff --git a/app/assets/javascripts/design_management_legacy/components/app.vue b/app/assets/javascripts/design_management_legacy/components/app.vue deleted file mode 100644 index 98240aef810..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/app.vue +++ /dev/null @@ -1,3 +0,0 @@ -<template> - <router-view /> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/delete_button.vue b/app/assets/javascripts/design_management_legacy/components/delete_button.vue deleted file mode 100644 index 1fd902c9ed7..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/delete_button.vue +++ /dev/null @@ -1,64 +0,0 @@ -<script> -import { GlDeprecatedButton, GlModal, GlModalDirective } from '@gitlab/ui'; -import { uniqueId } from 'lodash'; - -export default { - name: 'DeleteButton', - components: { - GlDeprecatedButton, - GlModal, - }, - directives: { - GlModalDirective, - }, - props: { - isDeleting: { - type: Boolean, - required: false, - default: false, - }, - buttonClass: { - type: String, - required: false, - default: '', - }, - buttonVariant: { - type: String, - required: false, - default: '', - }, - hasSelectedDesigns: { - type: Boolean, - required: false, - default: true, - }, - }, - data() { - return { - modalId: uniqueId('design-deletion-confirmation-'), - }; - }, -}; -</script> - -<template> - <div> - <gl-modal - :modal-id="modalId" - :title="s__('DesignManagement|Delete designs confirmation')" - :ok-title="s__('DesignManagement|Delete')" - ok-variant="danger" - @ok="$emit('deleteSelectedDesigns')" - > - <p>{{ s__('DesignManagement|Are you sure you want to delete the selected designs?') }}</p> - </gl-modal> - <gl-deprecated-button - v-gl-modal-directive="modalId" - :variant="buttonVariant" - :disabled="isDeleting || !hasSelectedDesigns" - :class="buttonClass" - > - <slot></slot> - </gl-deprecated-button> - </div> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/design_destroyer.vue b/app/assets/javascripts/design_management_legacy/components/design_destroyer.vue deleted file mode 100644 index 62460ca551c..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/design_destroyer.vue +++ /dev/null @@ -1,66 +0,0 @@ -<script> -import { ApolloMutation } from 'vue-apollo'; -import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql'; -import destroyDesignMutation from '../graphql/mutations/destroy_design.mutation.graphql'; -import { updateStoreAfterDesignsDelete } from '../utils/cache_update'; - -export default { - components: { - ApolloMutation, - }, - props: { - filenames: { - type: Array, - required: true, - }, - projectPath: { - type: String, - required: true, - }, - iid: { - type: String, - required: true, - }, - }, - computed: { - projectQueryBody() { - return { - query: getDesignListQuery, - variables: { fullPath: this.projectPath, iid: this.iid, atVersion: null }, - }; - }, - }, - methods: { - updateStoreAfterDelete( - store, - { - data: { designManagementDelete }, - }, - ) { - updateStoreAfterDesignsDelete( - store, - designManagementDelete, - this.projectQueryBody, - this.filenames, - ); - }, - }, - destroyDesignMutation, -}; -</script> - -<template> - <apollo-mutation - #default="{ mutate, loading, error }" - :mutation="$options.destroyDesignMutation" - :variables="{ - filenames, - projectPath, - iid, - }" - :update="updateStoreAfterDelete" - v-on="$listeners" - > - <slot v-bind="{ mutate, loading, error }"></slot> - </apollo-mutation> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/design_note_pin.vue b/app/assets/javascripts/design_management_legacy/components/design_note_pin.vue deleted file mode 100644 index 2b5e62c2870..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/design_note_pin.vue +++ /dev/null @@ -1,61 +0,0 @@ -<script> -import { GlIcon } from '@gitlab/ui'; -import { __, sprintf } from '~/locale'; - -export default { - name: 'DesignNotePin', - components: { - GlIcon, - }, - props: { - position: { - type: Object, - required: true, - }, - label: { - type: Number, - required: false, - default: null, - }, - repositioning: { - type: Boolean, - required: false, - default: false, - }, - }, - computed: { - isNewNote() { - return this.label === null; - }, - pinStyle() { - return this.repositioning ? { ...this.position, cursor: 'move' } : this.position; - }, - pinLabel() { - return this.isNewNote - ? __('Comment form position') - : sprintf(__("Comment '%{label}' position"), { label: this.label }); - }, - }, -}; -</script> - -<template> - <button - :style="pinStyle" - :aria-label="pinLabel" - :class="{ - 'btn-transparent comment-indicator': isNewNote, - 'js-image-badge badge badge-pill': !isNewNote, - }" - class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0" - type="button" - @mousedown="$emit('mousedown', $event)" - @mouseup="$emit('mouseup', $event)" - @click="$emit('click', $event)" - > - <gl-icon v-if="isNewNote" name="image-comment-dark" :size="24" /> - <template v-else> - {{ label }} - </template> - </button> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management_legacy/components/design_notes/design_discussion.vue deleted file mode 100644 index 6a20517eed7..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/design_notes/design_discussion.vue +++ /dev/null @@ -1,297 +0,0 @@ -<script> -import { ApolloMutation } from 'vue-apollo'; -import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink } from '@gitlab/ui'; -import { s__ } from '~/locale'; -import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; -import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import allVersionsMixin from '../../mixins/all_versions'; -import createNoteMutation from '../../graphql/mutations/create_note.mutation.graphql'; -import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql'; -import getDesignQuery from '../../graphql/queries/get_design.query.graphql'; -import activeDiscussionQuery from '../../graphql/queries/active_discussion.query.graphql'; -import DesignNote from './design_note.vue'; -import DesignReplyForm from './design_reply_form.vue'; -import { updateStoreAfterAddDiscussionComment } from '../../utils/cache_update'; -import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants'; -import ToggleRepliesWidget from './toggle_replies_widget.vue'; - -export default { - components: { - ApolloMutation, - DesignNote, - ReplyPlaceholder, - DesignReplyForm, - GlIcon, - GlLoadingIcon, - GlLink, - ToggleRepliesWidget, - TimeAgoTooltip, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - mixins: [allVersionsMixin], - props: { - discussion: { - type: Object, - required: true, - }, - noteableId: { - type: String, - required: true, - }, - designId: { - type: String, - required: true, - }, - markdownPreviewPath: { - type: String, - required: false, - default: '', - }, - resolvedDiscussionsExpanded: { - type: Boolean, - required: true, - }, - discussionWithOpenForm: { - type: String, - required: true, - }, - }, - apollo: { - activeDiscussion: { - query: activeDiscussionQuery, - result({ data }) { - const discussionId = data.activeDiscussion.id; - if (this.discussion.resolved && !this.resolvedDiscussionsExpanded) { - return; - } - // We watch any changes to the active discussion from the design pins and scroll to this discussion if it exists - // We don't want scrollIntoView to be triggered from the discussion click itself - if ( - discussionId && - data.activeDiscussion.source === ACTIVE_DISCUSSION_SOURCE_TYPES.pin && - discussionId === this.discussion.notes[0].id - ) { - this.$el.scrollIntoView({ - behavior: 'smooth', - inline: 'start', - }); - } - }, - }, - }, - data() { - return { - discussionComment: '', - isFormRendered: false, - activeDiscussion: {}, - isResolving: false, - shouldChangeResolvedStatus: false, - areRepliesCollapsed: this.discussion.resolved, - }; - }, - computed: { - mutationPayload() { - return { - noteableId: this.noteableId, - body: this.discussionComment, - discussionId: this.discussion.id, - }; - }, - designVariables() { - return { - fullPath: this.projectPath, - iid: this.issueIid, - filenames: [this.$route.params.id], - atVersion: this.designsVersion, - }; - }, - isDiscussionHighlighted() { - return this.discussion.notes[0].id === this.activeDiscussion.id; - }, - resolveCheckboxText() { - return this.discussion.resolved - ? s__('DesignManagement|Unresolve thread') - : s__('DesignManagement|Resolve thread'); - }, - firstNote() { - return this.discussion.notes[0]; - }, - discussionReplies() { - return this.discussion.notes.slice(1); - }, - areRepliesShown() { - return !this.discussion.resolved || !this.areRepliesCollapsed; - }, - resolveIconName() { - return this.discussion.resolved ? 'check-circle-filled' : 'check-circle'; - }, - isRepliesWidgetVisible() { - return this.discussion.resolved && this.discussionReplies.length > 0; - }, - isReplyPlaceholderVisible() { - return this.areRepliesShown || !this.discussionReplies.length; - }, - isFormVisible() { - return this.isFormRendered && this.discussionWithOpenForm === this.discussion.id; - }, - }, - methods: { - addDiscussionComment( - store, - { - data: { createNote }, - }, - ) { - updateStoreAfterAddDiscussionComment( - store, - createNote, - getDesignQuery, - this.designVariables, - this.discussion.id, - ); - }, - onDone() { - this.discussionComment = ''; - this.hideForm(); - if (this.shouldChangeResolvedStatus) { - this.toggleResolvedStatus(); - } - }, - onCreateNoteError(err) { - this.$emit('createNoteError', err); - }, - hideForm() { - this.isFormRendered = false; - this.discussionComment = ''; - }, - showForm() { - this.$emit('openForm', this.discussion.id); - this.isFormRendered = true; - }, - toggleResolvedStatus() { - this.isResolving = true; - this.$apollo - .mutate({ - mutation: toggleResolveDiscussionMutation, - variables: { id: this.discussion.id, resolve: !this.discussion.resolved }, - }) - .then(({ data }) => { - if (data.errors?.length > 0) { - this.$emit('resolveDiscussionError', data.errors[0]); - } - }) - .catch(err => { - this.$emit('resolveDiscussionError', err); - }) - .finally(() => { - this.isResolving = false; - }); - }, - }, - createNoteMutation, -}; -</script> - -<template> - <div class="design-discussion-wrapper"> - <div - class="badge badge-pill gl-display-flex gl-align-items-center gl-justify-content-center" - :class="{ resolved: discussion.resolved }" - type="button" - > - {{ discussion.index }} - </div> - <ul - class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none" - data-qa-selector="design_discussion_content" - > - <design-note - :note="firstNote" - :markdown-preview-path="markdownPreviewPath" - :is-resolving="isResolving" - :class="{ 'gl-bg-blue-50': isDiscussionHighlighted }" - @error="$emit('updateNoteError', $event)" - > - <template v-if="discussion.resolvable" #resolveDiscussion> - <button - v-gl-tooltip - :class="{ 'is-active': discussion.resolved }" - :title="resolveCheckboxText" - :aria-label="resolveCheckboxText" - type="button" - class="line-resolve-btn note-action-button gl-mr-3" - data-testid="resolve-button" - @click.stop="toggleResolvedStatus" - > - <gl-icon v-if="!isResolving" :name="resolveIconName" data-testid="resolve-icon" /> - <gl-loading-icon v-else inline /> - </button> - </template> - <template v-if="discussion.resolved" #resolvedStatus> - <p class="gl-text-gray-500 gl-font-sm gl-m-0 gl-mt-5" data-testid="resolved-message"> - {{ __('Resolved by') }} - <gl-link - class="gl-text-gray-500 gl-text-decoration-none gl-font-sm link-inherit-color" - :href="discussion.resolvedBy.webUrl" - target="_blank" - >{{ discussion.resolvedBy.name }}</gl-link - > - <time-ago-tooltip :time="discussion.resolvedAt" tooltip-placement="bottom" /> - </p> - </template> - </design-note> - <toggle-replies-widget - v-if="isRepliesWidgetVisible" - :collapsed="areRepliesCollapsed" - :replies="discussionReplies" - @toggle="areRepliesCollapsed = !areRepliesCollapsed" - /> - <design-note - v-for="note in discussionReplies" - v-show="areRepliesShown" - :key="note.id" - :note="note" - :markdown-preview-path="markdownPreviewPath" - :is-resolving="isResolving" - :class="{ 'gl-bg-blue-50': isDiscussionHighlighted }" - @error="$emit('updateNoteError', $event)" - /> - <li v-show="isReplyPlaceholderVisible" class="reply-wrapper"> - <reply-placeholder - v-if="!isFormVisible" - class="qa-discussion-reply" - :button-text="__('Reply...')" - @onClick="showForm" - /> - <apollo-mutation - v-else - #default="{ mutate, loading }" - :mutation="$options.createNoteMutation" - :variables="{ - input: mutationPayload, - }" - :update="addDiscussionComment" - @done="onDone" - @error="onCreateNoteError" - > - <design-reply-form - v-model="discussionComment" - :is-saving="loading" - :markdown-preview-path="markdownPreviewPath" - @submitForm="mutate" - @cancelForm="hideForm" - > - <template v-if="discussion.resolvable" #resolveCheckbox> - <label data-testid="resolve-checkbox"> - <input v-model="shouldChangeResolvedStatus" type="checkbox" /> - {{ resolveCheckboxText }} - </label> - </template> - </design-reply-form> - </apollo-mutation> - </li> - </ul> - </div> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/design_notes/design_note.vue b/app/assets/javascripts/design_management_legacy/components/design_notes/design_note.vue deleted file mode 100644 index b1f3a43a66d..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/design_notes/design_note.vue +++ /dev/null @@ -1,156 +0,0 @@ -<script> -import { ApolloMutation } from 'vue-apollo'; -import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; -import updateNoteMutation from '../../graphql/mutations/update_note.mutation.graphql'; -import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; -import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; -import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import DesignReplyForm from './design_reply_form.vue'; -import { findNoteId } from '../../utils/design_management_utils'; -import { hasErrors } from '../../utils/cache_update'; - -export default { - components: { - UserAvatarLink, - TimelineEntryItem, - TimeAgoTooltip, - DesignReplyForm, - ApolloMutation, - GlIcon, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - props: { - note: { - type: Object, - required: true, - }, - markdownPreviewPath: { - type: String, - required: false, - default: '', - }, - }, - data() { - return { - noteText: this.note.body, - isEditing: false, - }; - }, - computed: { - author() { - return this.note.author; - }, - noteAnchorId() { - return findNoteId(this.note.id); - }, - isNoteLinked() { - return this.$route.hash === `#note_${this.noteAnchorId}`; - }, - mutationPayload() { - return { - id: this.note.id, - body: this.noteText, - }; - }, - isEditButtonVisible() { - return !this.isEditing && this.note.userPermissions.adminNote; - }, - }, - mounted() { - if (this.isNoteLinked) { - this.$refs.anchor.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' }); - } - }, - methods: { - hideForm() { - this.isEditing = false; - this.noteText = this.note.body; - }, - onDone({ data }) { - this.hideForm(); - if (hasErrors(data.updateNote)) { - this.$emit('error', data.errors[0]); - } - }, - }, - updateNoteMutation, -}; -</script> - -<template> - <timeline-entry-item :id="`note_${noteAnchorId}`" ref="anchor" class="design-note note-form"> - <user-avatar-link - :link-href="author.webUrl" - :img-src="author.avatarUrl" - :img-alt="author.username" - :img-size="40" - /> - <div class="d-flex justify-content-between"> - <div> - <a - v-once - :href="author.webUrl" - class="js-user-link" - :data-user-id="author.id" - :data-username="author.username" - > - <span class="note-header-author-name bold">{{ author.name }}</span> - <span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span> - <span class="note-headline-light">@{{ author.username }}</span> - </a> - <span class="note-headline-light note-headline-meta"> - <span class="system-note-message"> <slot></slot> </span> - <template v-if="note.createdAt"> - <span class="system-note-separator"></span> - <a class="note-timestamp system-note-separator" :href="`#note_${noteAnchorId}`"> - <time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" /> - </a> - </template> - </span> - </div> - <div class="gl-display-flex"> - <slot name="resolveDiscussion"></slot> - <button - v-if="isEditButtonVisible" - v-gl-tooltip - type="button" - :title="__('Edit comment')" - class="note-action-button js-note-edit btn btn-transparent qa-note-edit-button" - @click="isEditing = true" - > - <gl-icon name="pencil" class="link-highlight" /> - </button> - </div> - </div> - <template v-if="!isEditing"> - <div - class="note-text js-note-text md" - data-qa-selector="note_content" - v-html="note.bodyHtml" - ></div> - <slot name="resolvedStatus"></slot> - </template> - <apollo-mutation - v-else - #default="{ mutate, loading }" - :mutation="$options.updateNoteMutation" - :variables="{ - input: mutationPayload, - }" - @error="$emit('error', $event)" - @done="onDone" - > - <design-reply-form - v-model="noteText" - :is-saving="loading" - :markdown-preview-path="markdownPreviewPath" - :is-new-comment="false" - class="mt-5" - @submitForm="mutate" - @cancelForm="hideForm" - /> - </apollo-mutation> - </timeline-entry-item> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management_legacy/components/design_notes/design_reply_form.vue deleted file mode 100644 index 969034909f2..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/design_notes/design_reply_form.vue +++ /dev/null @@ -1,141 +0,0 @@ -<script> -import { GlDeprecatedButton, GlModal } from '@gitlab/ui'; -import MarkdownField from '~/vue_shared/components/markdown/field.vue'; -import { s__ } from '~/locale'; - -export default { - name: 'DesignReplyForm', - components: { - MarkdownField, - GlDeprecatedButton, - GlModal, - }, - props: { - markdownPreviewPath: { - type: String, - required: false, - default: '', - }, - value: { - type: String, - required: true, - }, - isSaving: { - type: Boolean, - required: true, - }, - isNewComment: { - type: Boolean, - required: false, - default: true, - }, - }, - data() { - return { - formText: this.value, - }; - }, - computed: { - hasValue() { - return this.value.trim().length > 0; - }, - modalSettings() { - if (this.isNewComment) { - return { - title: s__('DesignManagement|Cancel comment confirmation'), - okTitle: s__('DesignManagement|Discard comment'), - cancelTitle: s__('DesignManagement|Keep comment'), - content: s__('DesignManagement|Are you sure you want to cancel creating this comment?'), - }; - } - return { - title: s__('DesignManagement|Cancel comment update confirmation'), - okTitle: s__('DesignManagement|Cancel changes'), - cancelTitle: s__('DesignManagement|Keep changes'), - content: s__('DesignManagement|Are you sure you want to cancel changes to this comment?'), - }; - }, - buttonText() { - return this.isNewComment - ? s__('DesignManagement|Comment') - : s__('DesignManagement|Save comment'); - }, - }, - mounted() { - this.focusInput(); - }, - methods: { - submitForm() { - if (this.hasValue) this.$emit('submitForm'); - }, - cancelComment() { - if (this.hasValue && this.formText !== this.value) { - this.$refs.cancelCommentModal.show(); - } else { - this.$emit('cancelForm'); - } - }, - focusInput() { - this.$refs.textarea.focus(); - }, - }, -}; -</script> - -<template> - <form class="new-note common-note-form" @submit.prevent> - <markdown-field - :markdown-preview-path="markdownPreviewPath" - :can-attach-file="false" - :enable-autocomplete="true" - :textarea-value="value" - markdown-docs-path="/help/user/markdown" - class="bordered-box" - > - <template #textarea> - <textarea - ref="textarea" - :value="value" - class="note-textarea js-gfm-input js-autosize markdown-area" - dir="auto" - data-supports-quick-actions="false" - data-qa-selector="note_textarea" - :aria-label="__('Description')" - :placeholder="__('Write a comment…')" - @input="$emit('input', $event.target.value)" - @keydown.meta.enter="submitForm" - @keydown.ctrl.enter="submitForm" - @keyup.esc.stop="cancelComment" - > - </textarea> - </template> - </markdown-field> - <slot name="resolveCheckbox"></slot> - <div class="note-form-actions gl-display-flex gl-justify-content-space-between"> - <gl-deprecated-button - ref="submitButton" - :disabled="!hasValue || isSaving" - variant="success" - type="submit" - data-track-event="click_button" - data-qa-selector="save_comment_button" - @click="$emit('submitForm')" - > - {{ buttonText }} - </gl-deprecated-button> - <gl-deprecated-button ref="cancelButton" @click="cancelComment">{{ - __('Cancel') - }}</gl-deprecated-button> - </div> - <gl-modal - ref="cancelCommentModal" - ok-variant="danger" - :title="modalSettings.title" - :ok-title="modalSettings.okTitle" - :cancel-title="modalSettings.cancelTitle" - modal-id="cancel-comment-modal" - @ok="$emit('cancelForm')" - >{{ modalSettings.content }} - </gl-modal> - </form> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/design_notes/toggle_replies_widget.vue b/app/assets/javascripts/design_management_legacy/components/design_notes/toggle_replies_widget.vue deleted file mode 100644 index 2e366282de3..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/design_notes/toggle_replies_widget.vue +++ /dev/null @@ -1,70 +0,0 @@ -<script> -import { GlIcon, GlButton, GlLink } from '@gitlab/ui'; -import { __, n__ } from '~/locale'; -import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; - -export default { - name: 'ToggleNotesWidget', - components: { - GlIcon, - GlButton, - GlLink, - TimeAgoTooltip, - }, - props: { - collapsed: { - type: Boolean, - required: true, - }, - replies: { - type: Array, - required: true, - }, - }, - computed: { - lastReply() { - return this.replies[this.replies.length - 1]; - }, - iconName() { - return this.collapsed ? 'chevron-right' : 'chevron-down'; - }, - toggleText() { - return this.collapsed - ? `${this.replies.length} ${n__('reply', 'replies', this.replies.length)}` - : __('Collapse replies'); - }, - }, -}; -</script> - -<template> - <li - class="toggle-comments gl-bg-gray-50 gl-display-flex gl-align-items-center gl-py-3" - :class="{ expanded: !collapsed }" - data-testid="toggle-comments-wrapper" - > - <gl-icon :name="iconName" class="gl-ml-3" @click.stop="$emit('toggle')" /> - <gl-button - variant="link" - class="toggle-comments-button gl-ml-2 gl-mr-2" - @click.stop="$emit('toggle')" - > - {{ toggleText }} - </gl-button> - <template v-if="collapsed"> - <span class="gl-text-gray-500">{{ __('Last reply by') }}</span> - <gl-link - :href="lastReply.author.webUrl" - target="_blank" - class="link-inherit-color gl-text-body gl-text-decoration-none gl-font-weight-bold gl-ml-2 gl-mr-2" - > - {{ lastReply.author.name }} - </gl-link> - <time-ago-tooltip - :time="lastReply.createdAt" - tooltip-placement="bottom" - class="gl-text-gray-500" - /> - </template> - </li> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/design_overlay.vue b/app/assets/javascripts/design_management_legacy/components/design_overlay.vue deleted file mode 100644 index 926e7c74802..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/design_overlay.vue +++ /dev/null @@ -1,287 +0,0 @@ -<script> -import activeDiscussionQuery from '../graphql/queries/active_discussion.query.graphql'; -import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql'; -import DesignNotePin from './design_note_pin.vue'; -import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants'; - -export default { - name: 'DesignOverlay', - components: { - DesignNotePin, - }, - props: { - dimensions: { - type: Object, - required: true, - }, - position: { - type: Object, - required: true, - }, - notes: { - type: Array, - required: false, - default: () => [], - }, - currentCommentForm: { - type: Object, - required: false, - default: null, - }, - disableCommenting: { - type: Boolean, - required: false, - default: false, - }, - resolvedDiscussionsExpanded: { - type: Boolean, - required: true, - }, - }, - apollo: { - activeDiscussion: { - query: activeDiscussionQuery, - }, - }, - data() { - return { - movingNoteNewPosition: null, - movingNoteStartPosition: null, - activeDiscussion: {}, - }; - }, - computed: { - overlayStyle() { - const cursor = this.disableCommenting ? 'unset' : undefined; - - return { - cursor, - width: `${this.dimensions.width}px`, - height: `${this.dimensions.height}px`, - ...this.position, - }; - }, - isMovingCurrentComment() { - return Boolean(this.movingNoteStartPosition && !this.movingNoteStartPosition.noteId); - }, - currentCommentPositionStyle() { - return this.isMovingCurrentComment && this.movingNoteNewPosition - ? this.getNotePositionStyle(this.movingNoteNewPosition) - : this.getNotePositionStyle(this.currentCommentForm); - }, - }, - methods: { - setNewNoteCoordinates({ x, y }) { - this.$emit('openCommentForm', { x, y }); - }, - getNoteRelativePosition(position) { - const { x, y, width, height } = position; - const widthRatio = this.dimensions.width / width; - const heightRatio = this.dimensions.height / height; - return { - left: Math.round(x * widthRatio), - top: Math.round(y * heightRatio), - }; - }, - getNotePositionStyle(position) { - const { left, top } = this.getNoteRelativePosition(position); - return { - left: `${left}px`, - top: `${top}px`, - }; - }, - getMovingNotePositionDelta(e) { - let deltaX = 0; - let deltaY = 0; - - if (this.movingNoteStartPosition) { - const { clientX, clientY } = this.movingNoteStartPosition; - deltaX = e.clientX - clientX; - deltaY = e.clientY - clientY; - } - - return { - deltaX, - deltaY, - }; - }, - isMovingNote(noteId) { - const movingNoteId = this.movingNoteStartPosition?.noteId; - return Boolean(movingNoteId && movingNoteId === noteId); - }, - canMoveNote(note) { - const { userPermissions } = note; - const { adminNote } = userPermissions || {}; - - return Boolean(adminNote); - }, - isPositionInOverlay(position) { - const { top, left } = this.getNoteRelativePosition(position); - const { height, width } = this.dimensions; - - return top >= 0 && top <= height && left >= 0 && left <= width; - }, - onNewNoteMove(e) { - if (!this.isMovingCurrentComment) return; - - const { deltaX, deltaY } = this.getMovingNotePositionDelta(e); - const x = this.currentCommentForm.x + deltaX; - const y = this.currentCommentForm.y + deltaY; - - const movingNoteNewPosition = { - x, - y, - width: this.dimensions.width, - height: this.dimensions.height, - }; - - if (!this.isPositionInOverlay(movingNoteNewPosition)) { - this.onNewNoteMouseup(); - return; - } - - this.movingNoteNewPosition = movingNoteNewPosition; - }, - onExistingNoteMove(e) { - const note = this.notes.find(({ id }) => id === this.movingNoteStartPosition.noteId); - if (!note || !this.canMoveNote(note)) return; - - const { position } = note; - const { width, height } = position; - const widthRatio = this.dimensions.width / width; - const heightRatio = this.dimensions.height / height; - - const { deltaX, deltaY } = this.getMovingNotePositionDelta(e); - const x = position.x * widthRatio + deltaX; - const y = position.y * heightRatio + deltaY; - - const movingNoteNewPosition = { - x, - y, - width: this.dimensions.width, - height: this.dimensions.height, - }; - - if (!this.isPositionInOverlay(movingNoteNewPosition)) { - this.onExistingNoteMouseup(); - return; - } - - this.movingNoteNewPosition = movingNoteNewPosition; - }, - onNewNoteMouseup() { - if (!this.movingNoteNewPosition) return; - - const { x, y } = this.movingNoteNewPosition; - this.setNewNoteCoordinates({ x, y }); - }, - onExistingNoteMouseup(note) { - if (!this.movingNoteStartPosition || !this.movingNoteNewPosition) { - this.updateActiveDiscussion(note.id); - this.$emit('closeCommentForm'); - return; - } - - const { x, y } = this.movingNoteNewPosition; - this.$emit('moveNote', { - noteId: this.movingNoteStartPosition.noteId, - discussionId: this.movingNoteStartPosition.discussionId, - coordinates: { x, y }, - }); - }, - onNoteMousedown({ clientX, clientY }, note) { - this.movingNoteStartPosition = { - noteId: note?.id, - discussionId: note?.discussion.id, - clientX, - clientY, - }; - }, - onOverlayMousemove(e) { - if (!this.movingNoteStartPosition) return; - - if (this.isMovingCurrentComment) { - this.onNewNoteMove(e); - } else { - this.onExistingNoteMove(e); - } - }, - onNoteMouseup(note) { - if (!this.movingNoteStartPosition) return; - - if (this.isMovingCurrentComment) { - this.onNewNoteMouseup(); - } else { - this.onExistingNoteMouseup(note); - } - - this.movingNoteStartPosition = null; - this.movingNoteNewPosition = null; - }, - onAddCommentMouseup({ offsetX, offsetY }) { - if (this.disableCommenting) return; - if (this.activeDiscussion.id) { - this.updateActiveDiscussion(); - } - - this.setNewNoteCoordinates({ x: offsetX, y: offsetY }); - }, - updateActiveDiscussion(id) { - this.$apollo.mutate({ - mutation: updateActiveDiscussionMutation, - variables: { - id, - source: ACTIVE_DISCUSSION_SOURCE_TYPES.pin, - }, - }); - }, - isNoteInactive(note) { - return this.activeDiscussion.id && this.activeDiscussion.id !== note.id; - }, - designPinClass(note) { - return { inactive: this.isNoteInactive(note), resolved: note.resolved }; - }, - }, -}; -</script> - -<template> - <div - class="position-absolute image-diff-overlay frame" - :style="overlayStyle" - @mousemove="onOverlayMousemove" - @mouseleave="onNoteMouseup" - > - <button - v-show="!disableCommenting" - type="button" - class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button" - data-qa-selector="design_image_button" - @mouseup="onAddCommentMouseup" - ></button> - <template v-for="note in notes"> - <design-note-pin - v-if="resolvedDiscussionsExpanded || !note.resolved" - :key="note.id" - :label="note.index" - :repositioning="isMovingNote(note.id)" - :position=" - isMovingNote(note.id) && movingNoteNewPosition - ? getNotePositionStyle(movingNoteNewPosition) - : getNotePositionStyle(note.position) - " - :class="designPinClass(note)" - @mousedown.stop="onNoteMousedown($event, note)" - @mouseup.stop="onNoteMouseup(note)" - /> - </template> - - <design-note-pin - v-if="currentCommentForm" - :position="currentCommentPositionStyle" - :repositioning="isMovingCurrentComment" - @mousedown.stop="onNoteMousedown" - @mouseup.stop="onNoteMouseup" - /> - </div> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/design_presentation.vue b/app/assets/javascripts/design_management_legacy/components/design_presentation.vue deleted file mode 100644 index 84dbb2809d9..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/design_presentation.vue +++ /dev/null @@ -1,322 +0,0 @@ -<script> -import { throttle } from 'lodash'; -import DesignImage from './image.vue'; -import DesignOverlay from './design_overlay.vue'; - -const CLICK_DRAG_BUFFER_PX = 2; - -export default { - components: { - DesignImage, - DesignOverlay, - }, - props: { - image: { - type: String, - required: false, - default: '', - }, - imageName: { - type: String, - required: false, - default: '', - }, - discussions: { - type: Array, - required: true, - }, - isAnnotating: { - type: Boolean, - required: false, - default: false, - }, - scale: { - type: Number, - required: false, - default: 1, - }, - resolvedDiscussionsExpanded: { - type: Boolean, - required: true, - }, - }, - data() { - return { - overlayDimensions: null, - overlayPosition: null, - currentAnnotationPosition: null, - zoomFocalPoint: { - x: 0, - y: 0, - width: 0, - height: 0, - }, - initialLoad: true, - lastDragPosition: null, - isDraggingDesign: false, - }; - }, - computed: { - discussionStartingNotes() { - return this.discussions.map(discussion => ({ - ...discussion.notes[0], - index: discussion.index, - })); - }, - currentCommentForm() { - return (this.isAnnotating && this.currentAnnotationPosition) || null; - }, - presentationStyle() { - return { - cursor: this.isDraggingDesign ? 'grabbing' : undefined, - }; - }, - }, - beforeDestroy() { - const { presentationViewport } = this.$refs; - if (!presentationViewport) return; - - presentationViewport.removeEventListener('scroll', this.scrollThrottled, false); - }, - mounted() { - const { presentationViewport } = this.$refs; - if (!presentationViewport) return; - - this.scrollThrottled = throttle(() => { - this.shiftZoomFocalPoint(); - }, 400); - - presentationViewport.addEventListener('scroll', this.scrollThrottled, false); - }, - methods: { - syncCurrentAnnotationPosition() { - if (!this.currentAnnotationPosition) return; - - const widthRatio = this.overlayDimensions.width / this.currentAnnotationPosition.width; - const heightRatio = this.overlayDimensions.height / this.currentAnnotationPosition.height; - const x = this.currentAnnotationPosition.x * widthRatio; - const y = this.currentAnnotationPosition.y * heightRatio; - - this.currentAnnotationPosition = this.getAnnotationPositon({ x, y }); - }, - setOverlayDimensions(overlayDimensions) { - this.overlayDimensions = overlayDimensions; - - // every time we set overlay dimensions, we need to - // update the current annotation as well - this.syncCurrentAnnotationPosition(); - }, - setOverlayPosition() { - if (!this.overlayDimensions) { - this.overlayPosition = {}; - } - - const { presentationViewport } = this.$refs; - if (!presentationViewport) return; - - // default to center - this.overlayPosition = { - left: `calc(50% - ${this.overlayDimensions.width / 2}px)`, - top: `calc(50% - ${this.overlayDimensions.height / 2}px)`, - }; - - // if the overlay overflows, then don't center - if (this.overlayDimensions.width > presentationViewport.offsetWidth) { - this.overlayPosition.left = '0'; - } - if (this.overlayDimensions.height > presentationViewport.offsetHeight) { - this.overlayPosition.top = '0'; - } - }, - /** - * Return a point that represents the center of an - * overflowing child element w.r.t it's parent - */ - getViewportCenter() { - const { presentationViewport } = this.$refs; - if (!presentationViewport) return {}; - - // get height of scroll bars (i.e. the max values for scrollTop, scrollLeft) - const scrollBarWidth = presentationViewport.scrollWidth - presentationViewport.offsetWidth; - const scrollBarHeight = presentationViewport.scrollHeight - presentationViewport.offsetHeight; - - // determine how many child pixels have been scrolled - const xScrollRatio = - presentationViewport.scrollLeft > 0 ? presentationViewport.scrollLeft / scrollBarWidth : 0; - const yScrollRatio = - presentationViewport.scrollTop > 0 ? presentationViewport.scrollTop / scrollBarHeight : 0; - const xScrollOffset = - (presentationViewport.scrollWidth - presentationViewport.offsetWidth - 0) * xScrollRatio; - const yScrollOffset = - (presentationViewport.scrollHeight - presentationViewport.offsetHeight - 0) * yScrollRatio; - - const viewportCenterX = presentationViewport.offsetWidth / 2; - const viewportCenterY = presentationViewport.offsetHeight / 2; - const focalPointX = viewportCenterX + xScrollOffset; - const focalPointY = viewportCenterY + yScrollOffset; - - return { - x: focalPointX, - y: focalPointY, - }; - }, - /** - * Scroll the viewport such that the focal point is positioned centrally - */ - scrollToFocalPoint() { - const { presentationViewport } = this.$refs; - if (!presentationViewport) return; - - const scrollX = this.zoomFocalPoint.x - presentationViewport.offsetWidth / 2; - const scrollY = this.zoomFocalPoint.y - presentationViewport.offsetHeight / 2; - - presentationViewport.scrollTo(scrollX, scrollY); - }, - scaleZoomFocalPoint() { - const { x, y, width, height } = this.zoomFocalPoint; - const widthRatio = this.overlayDimensions.width / width; - const heightRatio = this.overlayDimensions.height / height; - - this.zoomFocalPoint = { - x: Math.round(x * widthRatio * 100) / 100, - y: Math.round(y * heightRatio * 100) / 100, - ...this.overlayDimensions, - }; - }, - shiftZoomFocalPoint() { - this.zoomFocalPoint = { - ...this.getViewportCenter(), - ...this.overlayDimensions, - }; - }, - onImageResize(imageDimensions) { - this.setOverlayDimensions(imageDimensions); - this.setOverlayPosition(); - - this.$nextTick(() => { - if (this.initialLoad) { - // set focal point on initial load - this.shiftZoomFocalPoint(); - this.initialLoad = false; - } else { - this.scaleZoomFocalPoint(); - this.scrollToFocalPoint(); - } - }); - }, - getAnnotationPositon(coordinates) { - const { x, y } = coordinates; - const { width, height } = this.overlayDimensions; - return { - x: Math.round(x), - y: Math.round(y), - width: Math.round(width), - height: Math.round(height), - }; - }, - openCommentForm(coordinates) { - this.currentAnnotationPosition = this.getAnnotationPositon(coordinates); - this.$emit('openCommentForm', this.currentAnnotationPosition); - }, - closeCommentForm() { - this.currentAnnotationPosition = null; - this.$emit('closeCommentForm'); - }, - moveNote({ noteId, discussionId, coordinates }) { - const position = this.getAnnotationPositon(coordinates); - this.$emit('moveNote', { noteId, discussionId, position }); - }, - onPresentationMousedown({ clientX, clientY }) { - if (!this.isDesignOverflowing()) return; - - this.lastDragPosition = { - x: clientX, - y: clientY, - }; - }, - getDragDelta(clientX, clientY) { - return { - deltaX: this.lastDragPosition.x - clientX, - deltaY: this.lastDragPosition.y - clientY, - }; - }, - exceedsDragThreshold(clientX, clientY) { - const { deltaX, deltaY } = this.getDragDelta(clientX, clientY); - - return Math.abs(deltaX) > CLICK_DRAG_BUFFER_PX || Math.abs(deltaY) > CLICK_DRAG_BUFFER_PX; - }, - shouldDragDesign(clientX, clientY) { - return ( - this.lastDragPosition && - (this.isDraggingDesign || this.exceedsDragThreshold(clientX, clientY)) - ); - }, - onPresentationMousemove({ clientX, clientY }) { - const { presentationViewport } = this.$refs; - if (!presentationViewport || !this.shouldDragDesign(clientX, clientY)) return; - - this.isDraggingDesign = true; - - const { scrollLeft, scrollTop } = presentationViewport; - const { deltaX, deltaY } = this.getDragDelta(clientX, clientY); - presentationViewport.scrollTo(scrollLeft + deltaX, scrollTop + deltaY); - - this.lastDragPosition = { - x: clientX, - y: clientY, - }; - }, - onPresentationMouseup() { - this.lastDragPosition = null; - this.isDraggingDesign = false; - }, - isDesignOverflowing() { - const { presentationViewport } = this.$refs; - if (!presentationViewport) return false; - - return ( - presentationViewport.scrollWidth > presentationViewport.offsetWidth || - presentationViewport.scrollHeight > presentationViewport.offsetHeight - ); - }, - }, -}; -</script> - -<template> - <div - ref="presentationViewport" - class="h-100 w-100 p-3 overflow-auto position-relative" - :style="presentationStyle" - @mousedown="onPresentationMousedown" - @mousemove="onPresentationMousemove" - @mouseup="onPresentationMouseup" - @mouseleave="onPresentationMouseup" - @touchstart="onPresentationMousedown" - @touchmove="onPresentationMousemove" - @touchend="onPresentationMouseup" - @touchcancel="onPresentationMouseup" - > - <div class="h-100 w-100 d-flex align-items-center position-relative"> - <design-image - v-if="image" - :image="image" - :name="imageName" - :scale="scale" - @resize="onImageResize" - /> - <design-overlay - v-if="overlayDimensions && overlayPosition" - :dimensions="overlayDimensions" - :position="overlayPosition" - :notes="discussionStartingNotes" - :current-comment-form="currentCommentForm" - :disable-commenting="isDraggingDesign" - :resolved-discussions-expanded="resolvedDiscussionsExpanded" - @openCommentForm="openCommentForm" - @closeCommentForm="closeCommentForm" - @moveNote="moveNote" - /> - </div> - </div> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/design_scaler.vue b/app/assets/javascripts/design_management_legacy/components/design_scaler.vue deleted file mode 100644 index 55dee74bef5..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/design_scaler.vue +++ /dev/null @@ -1,65 +0,0 @@ -<script> -import { GlIcon } from '@gitlab/ui'; - -const SCALE_STEP_SIZE = 0.2; -const DEFAULT_SCALE = 1; -const MIN_SCALE = 1; -const MAX_SCALE = 2; - -export default { - components: { - GlIcon, - }, - data() { - return { - scale: DEFAULT_SCALE, - }; - }, - computed: { - disableReset() { - return this.scale <= MIN_SCALE; - }, - disableDecrease() { - return this.scale === DEFAULT_SCALE; - }, - disableIncrease() { - return this.scale >= MAX_SCALE; - }, - }, - methods: { - setScale(scale) { - if (scale < MIN_SCALE) { - return; - } - - this.scale = Math.round(scale * 100) / 100; - this.$emit('scale', this.scale); - }, - incrementScale() { - this.setScale(this.scale + SCALE_STEP_SIZE); - }, - decrementScale() { - this.setScale(this.scale - SCALE_STEP_SIZE); - }, - resetScale() { - this.setScale(DEFAULT_SCALE); - }, - }, -}; -</script> - -<template> - <div class="design-scaler btn-group" role="group"> - <button class="btn" :disabled="disableDecrease" @click="decrementScale"> - <span class="d-flex-center gl-icon s16"> - – - </span> - </button> - <button class="btn" :disabled="disableReset" @click="resetScale"> - <gl-icon name="redo" /> - </button> - <button class="btn" :disabled="disableIncrease" @click="incrementScale"> - <gl-icon name="plus" /> - </button> - </div> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/design_sidebar.vue b/app/assets/javascripts/design_management_legacy/components/design_sidebar.vue deleted file mode 100644 index 622120e2008..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/design_sidebar.vue +++ /dev/null @@ -1,178 +0,0 @@ -<script> -import Cookies from 'js-cookie'; -import { GlCollapse, GlButton, GlPopover } from '@gitlab/ui'; -import { s__ } from '~/locale'; -import { parseBoolean } from '~/lib/utils/common_utils'; -import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql'; -import { extractDiscussions, extractParticipants } from '../utils/design_management_utils'; -import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants'; -import DesignDiscussion from './design_notes/design_discussion.vue'; -import Participants from '~/sidebar/components/participants/participants.vue'; - -export default { - components: { - DesignDiscussion, - Participants, - GlCollapse, - GlButton, - GlPopover, - }, - props: { - design: { - type: Object, - required: true, - }, - resolvedDiscussionsExpanded: { - type: Boolean, - required: true, - }, - markdownPreviewPath: { - type: String, - required: true, - }, - }, - data() { - return { - isResolvedCommentsPopoverHidden: parseBoolean(Cookies.get(this.$options.cookieKey)), - discussionWithOpenForm: '', - }; - }, - computed: { - discussions() { - return extractDiscussions(this.design.discussions); - }, - issue() { - return { - ...this.design.issue, - webPath: this.design.issue.webPath.substr(1), - }; - }, - discussionParticipants() { - return extractParticipants(this.issue.participants); - }, - resolvedDiscussions() { - return this.discussions.filter(discussion => discussion.resolved); - }, - unresolvedDiscussions() { - return this.discussions.filter(discussion => !discussion.resolved); - }, - resolvedCommentsToggleIcon() { - return this.resolvedDiscussionsExpanded ? 'chevron-down' : 'chevron-right'; - }, - }, - methods: { - handleSidebarClick() { - this.isResolvedCommentsPopoverHidden = true; - Cookies.set(this.$options.cookieKey, 'true', { expires: 365 * 10 }); - this.updateActiveDiscussion(); - }, - updateActiveDiscussion(id) { - this.$apollo.mutate({ - mutation: updateActiveDiscussionMutation, - variables: { - id, - source: ACTIVE_DISCUSSION_SOURCE_TYPES.discussion, - }, - }); - }, - closeCommentForm() { - this.comment = ''; - this.$emit('closeCommentForm'); - }, - updateDiscussionWithOpenForm(id) { - this.discussionWithOpenForm = id; - }, - }, - resolveCommentsToggleText: s__('DesignManagement|Resolved Comments'), - cookieKey: 'hide_design_resolved_comments_popover', -}; -</script> - -<template> - <div class="image-notes" @click="handleSidebarClick"> - <h2 class="gl-font-weight-bold gl-mt-0"> - {{ issue.title }} - </h2> - <a - class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block" - :href="issue.webUrl" - >{{ issue.webPath }}</a - > - <participants - :participants="discussionParticipants" - :show-participant-label="false" - class="gl-mb-4" - /> - <h2 - v-if="unresolvedDiscussions.length === 0" - class="new-discussion-disclaimer gl-font-base gl-m-0 gl-mb-4" - data-testid="new-discussion-disclaimer" - > - {{ s__("DesignManagement|Click the image where you'd like to start a new discussion") }} - </h2> - <design-discussion - v-for="discussion in unresolvedDiscussions" - :key="discussion.id" - :discussion="discussion" - :design-id="$route.params.id" - :noteable-id="design.id" - :markdown-preview-path="markdownPreviewPath" - :resolved-discussions-expanded="resolvedDiscussionsExpanded" - :discussion-with-open-form="discussionWithOpenForm" - data-testid="unresolved-discussion" - @createNoteError="$emit('onDesignDiscussionError', $event)" - @updateNoteError="$emit('updateNoteError', $event)" - @resolveDiscussionError="$emit('resolveDiscussionError', $event)" - @click.native.stop="updateActiveDiscussion(discussion.notes[0].id)" - @openForm="updateDiscussionWithOpenForm" - /> - <template v-if="resolvedDiscussions.length > 0"> - <gl-button - id="resolved-comments" - data-testid="resolved-comments" - :icon="resolvedCommentsToggleIcon" - variant="link" - class="link-inherit-color gl-text-body gl-text-decoration-none gl-font-weight-bold gl-mb-4" - @click="$emit('toggleResolvedComments')" - >{{ $options.resolveCommentsToggleText }} ({{ resolvedDiscussions.length }}) - </gl-button> - <gl-popover - v-if="!isResolvedCommentsPopoverHidden" - :show="!isResolvedCommentsPopoverHidden" - target="resolved-comments" - container="popovercontainer" - placement="top" - :title="s__('DesignManagement|Resolved Comments')" - > - <p> - {{ - s__( - 'DesignManagement|Comments you resolve can be viewed and unresolved by going to the "Resolved Comments" section below', - ) - }} - </p> - <a href="#" rel="noopener noreferrer" target="_blank">{{ - s__('DesignManagement|Learn more about resolving comments') - }}</a> - </gl-popover> - <gl-collapse :visible="resolvedDiscussionsExpanded" class="gl-mt-3"> - <design-discussion - v-for="discussion in resolvedDiscussions" - :key="discussion.id" - :discussion="discussion" - :design-id="$route.params.id" - :noteable-id="design.id" - :markdown-preview-path="markdownPreviewPath" - :resolved-discussions-expanded="resolvedDiscussionsExpanded" - :discussion-with-open-form="discussionWithOpenForm" - data-testid="resolved-discussion" - @error="$emit('onDesignDiscussionError', $event)" - @updateNoteError="$emit('updateNoteError', $event)" - @openForm="updateDiscussionWithOpenForm" - @click.native.stop="updateActiveDiscussion(discussion.notes[0].id)" - /> - </gl-collapse> - </template> - <slot name="replyForm"></slot> - </div> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/image.vue b/app/assets/javascripts/design_management_legacy/components/image.vue deleted file mode 100644 index 91b7b576e0c..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/image.vue +++ /dev/null @@ -1,110 +0,0 @@ -<script> -import { throttle } from 'lodash'; -import { GlIcon } from '@gitlab/ui'; - -export default { - components: { - GlIcon, - }, - props: { - image: { - type: String, - required: false, - default: '', - }, - name: { - type: String, - required: false, - default: '', - }, - scale: { - type: Number, - required: false, - default: 1, - }, - }, - data() { - return { - baseImageSize: null, - imageStyle: null, - imageError: false, - }; - }, - watch: { - scale(val) { - this.zoom(val); - }, - }, - beforeDestroy() { - window.removeEventListener('resize', this.resizeThrottled, false); - }, - mounted() { - this.onImgLoad(); - - this.resizeThrottled = throttle(() => { - // NOTE: if imageStyle is set, then baseImageSize - // won't change due to resize. We must still emit a - // `resize` event so that the parent can handle - // resizes appropriately (e.g. for design_overlay) - this.setBaseImageSize(); - }, 400); - window.addEventListener('resize', this.resizeThrottled, false); - }, - methods: { - onImgLoad() { - requestIdleCallback(this.setBaseImageSize, { timeout: 1000 }); - }, - onImgError() { - this.imageError = true; - }, - setBaseImageSize() { - const { contentImg } = this.$refs; - if (!contentImg || contentImg.offsetHeight === 0 || contentImg.offsetWidth === 0) return; - - this.baseImageSize = { - height: contentImg.offsetHeight, - width: contentImg.offsetWidth, - }; - this.onResize({ width: this.baseImageSize.width, height: this.baseImageSize.height }); - }, - onResize({ width, height }) { - this.$emit('resize', { width, height }); - }, - zoom(amount) { - if (amount === 1) { - this.imageStyle = null; - this.$nextTick(() => { - this.setBaseImageSize(); - }); - return; - } - const width = this.baseImageSize.width * amount; - const height = this.baseImageSize.height * amount; - - this.imageStyle = { - width: `${width}px`, - height: `${height}px`, - }; - - this.onResize({ width, height }); - }, - }, -}; -</script> - -<template> - <div class="m-auto js-design-image"> - <gl-icon v-if="imageError" class="text-secondary-100" name="media-broken" :size="48" /> - <img - v-show="!imageError" - ref="contentImg" - class="mh-100" - :src="image" - :alt="name" - :style="imageStyle" - :class="{ 'img-fluid': !imageStyle }" - @error="onImgError" - @load="onImgLoad" - /> - </div> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/list/item.vue b/app/assets/javascripts/design_management_legacy/components/list/item.vue deleted file mode 100644 index 13c703b8a88..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/list/item.vue +++ /dev/null @@ -1,174 +0,0 @@ -<script> -import { GlLoadingIcon, GlIcon, GlIntersectionObserver } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; -import Timeago from '~/vue_shared/components/time_ago_tooltip.vue'; -import { n__, __ } from '~/locale'; -import { DESIGN_ROUTE_NAME } from '../../router/constants'; - -export default { - components: { - GlLoadingIcon, - GlIntersectionObserver, - GlIcon, - Icon, - Timeago, - }, - props: { - id: { - type: [Number, String], - required: true, - }, - event: { - type: String, - required: true, - }, - notesCount: { - type: Number, - required: true, - }, - image: { - type: String, - required: true, - }, - filename: { - type: String, - required: true, - }, - updatedAt: { - type: String, - required: false, - default: null, - }, - isUploading: { - type: Boolean, - required: false, - default: true, - }, - imageV432x230: { - type: String, - required: false, - default: null, - }, - }, - data() { - return { - imageLoading: true, - imageError: false, - wasInView: false, - }; - }, - computed: { - icon() { - const normalizedEvent = this.event.toLowerCase(); - const icons = { - creation: { - name: 'file-addition-solid', - classes: 'text-success-500', - tooltip: __('Added in this version'), - }, - modification: { - name: 'file-modified-solid', - classes: 'text-primary-500', - tooltip: __('Modified in this version'), - }, - deletion: { - name: 'file-deletion-solid', - classes: 'text-danger-500', - tooltip: __('Deleted in this version'), - }, - }; - - return icons[normalizedEvent] ? icons[normalizedEvent] : {}; - }, - notesLabel() { - return n__('%d comment', '%d comments', this.notesCount); - }, - imageLink() { - return this.wasInView ? this.imageV432x230 || this.image : ''; - }, - showLoadingSpinner() { - return this.imageLoading || this.isUploading; - }, - showImageErrorIcon() { - return this.wasInView && this.imageError; - }, - showImage() { - return !this.showLoadingSpinner && !this.showImageErrorIcon; - }, - }, - methods: { - onImageLoad() { - this.imageLoading = false; - this.imageError = false; - }, - onImageError() { - this.imageLoading = false; - this.imageError = true; - }, - onAppear() { - // do nothing if image has previously - // been in view - if (this.wasInView) { - return; - } - - this.wasInView = true; - this.imageLoading = true; - }, - }, - DESIGN_ROUTE_NAME, -}; -</script> - -<template> - <router-link - :to="{ - name: $options.DESIGN_ROUTE_NAME, - params: { id: filename }, - query: $route.query, - }" - class="card cursor-pointer text-plain js-design-list-item design-list-item" - > - <div class="card-body p-0 d-flex-center overflow-hidden position-relative"> - <div v-if="icon.name" data-testid="designEvent" class="design-event position-absolute"> - <span :title="icon.tooltip" :aria-label="icon.tooltip"> - <icon :name="icon.name" :size="18" :class="icon.classes" /> - </span> - </div> - <gl-intersection-observer @appear="onAppear"> - <gl-loading-icon v-if="showLoadingSpinner" size="md" /> - <gl-icon - v-else-if="showImageErrorIcon" - name="media-broken" - class="text-secondary" - :size="32" - /> - <img - v-show="showImage" - :src="imageLink" - :alt="filename" - class="block mx-auto mw-100 mh-100 design-img" - data-qa-selector="design_image" - @load="onImageLoad" - @error="onImageError" - /> - </gl-intersection-observer> - </div> - <div class="card-footer d-flex w-100"> - <div class="d-flex flex-column str-truncated-100"> - <span class="bold str-truncated-100" data-qa-selector="design_file_name">{{ - filename - }}</span> - <span v-if="updatedAt" class="str-truncated-100"> - {{ __('Updated') }} <timeago :time="updatedAt" tooltip-placement="bottom" /> - </span> - </div> - <div v-if="notesCount" class="ml-auto d-flex align-items-center text-secondary"> - <icon name="comments" class="ml-1" /> - <span :aria-label="notesLabel" class="ml-1"> - {{ notesCount }} - </span> - </div> - </div> - </router-link> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/toolbar/index.vue b/app/assets/javascripts/design_management_legacy/components/toolbar/index.vue deleted file mode 100644 index b998dfc47b8..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/toolbar/index.vue +++ /dev/null @@ -1,126 +0,0 @@ -<script> -import { GlDeprecatedButton } from '@gitlab/ui'; -import { __, sprintf } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; -import timeagoMixin from '~/vue_shared/mixins/timeago'; -import Pagination from './pagination.vue'; -import DeleteButton from '../delete_button.vue'; -import permissionsQuery from '../../graphql/queries/design_permissions.query.graphql'; -import appDataQuery from '../../graphql/queries/app_data.query.graphql'; -import { DESIGNS_ROUTE_NAME } from '../../router/constants'; - -export default { - components: { - Icon, - Pagination, - DeleteButton, - GlDeprecatedButton, - }, - mixins: [timeagoMixin], - props: { - id: { - type: String, - required: true, - }, - isDeleting: { - type: Boolean, - required: true, - }, - filename: { - type: String, - required: false, - default: '', - }, - updatedAt: { - type: String, - required: false, - default: null, - }, - updatedBy: { - type: Object, - required: false, - default: () => ({}), - }, - isLatestVersion: { - type: Boolean, - required: true, - }, - image: { - type: String, - required: true, - }, - }, - data() { - return { - permissions: { - createDesign: false, - }, - projectPath: '', - issueIid: null, - }; - }, - apollo: { - appData: { - query: appDataQuery, - manual: true, - result({ data: { projectPath, issueIid } }) { - this.projectPath = projectPath; - this.issueIid = issueIid; - }, - }, - permissions: { - query: permissionsQuery, - variables() { - return { - fullPath: this.projectPath, - iid: this.issueIid, - }; - }, - update: data => data.project.issue.userPermissions, - }, - }, - computed: { - updatedText() { - return sprintf(__('Updated %{updated_at} by %{updated_by}'), { - updated_at: this.timeFormatted(this.updatedAt), - updated_by: this.updatedBy.name, - }); - }, - canDeleteDesign() { - return this.permissions.createDesign; - }, - }, - DESIGNS_ROUTE_NAME, -}; -</script> - -<template> - <header class="d-flex p-2 bg-white align-items-center js-design-header"> - <router-link - :to="{ - name: $options.DESIGNS_ROUTE_NAME, - query: $route.query, - }" - :aria-label="s__('DesignManagement|Go back to designs')" - class="mr-3 text-plain d-flex justify-content-center align-items-center" - > - <icon :size="18" name="close" /> - </router-link> - <div class="overflow-hidden d-flex align-items-center"> - <h2 class="m-0 str-truncated-100 gl-font-base">{{ filename }}</h2> - <small v-if="updatedAt" class="text-secondary">{{ updatedText }}</small> - </div> - <pagination :id="id" class="ml-auto flex-shrink-0" /> - <gl-deprecated-button :href="image" class="mr-2"> - <icon :size="18" name="download" /> - </gl-deprecated-button> - <delete-button - v-if="isLatestVersion && canDeleteDesign" - :is-deleting="isDeleting" - button-variant="danger" - @deleteSelectedDesigns="$emit('delete')" - > - <icon :size="18" name="remove" /> - </delete-button> - </header> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/toolbar/pagination.vue b/app/assets/javascripts/design_management_legacy/components/toolbar/pagination.vue deleted file mode 100644 index bf62a8f66a6..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/toolbar/pagination.vue +++ /dev/null @@ -1,83 +0,0 @@ -<script> -/* global Mousetrap */ -import 'mousetrap'; -import { s__, sprintf } from '~/locale'; -import PaginationButton from './pagination_button.vue'; -import allDesignsMixin from '../../mixins/all_designs'; -import { DESIGN_ROUTE_NAME } from '../../router/constants'; - -export default { - components: { - PaginationButton, - }, - mixins: [allDesignsMixin], - props: { - id: { - type: String, - required: true, - }, - }, - computed: { - designsCount() { - return this.designs.length; - }, - currentIndex() { - return this.designs.findIndex(design => design.filename === this.id); - }, - paginationText() { - return sprintf(s__('DesignManagement|%{current_design} of %{designs_count}'), { - current_design: this.currentIndex + 1, - designs_count: this.designsCount, - }); - }, - previousDesign() { - if (!this.designsCount) return null; - - return this.designs[this.currentIndex - 1]; - }, - nextDesign() { - if (!this.designsCount) return null; - - return this.designs[this.currentIndex + 1]; - }, - }, - mounted() { - Mousetrap.bind('left', () => this.navigateToDesign(this.previousDesign)); - Mousetrap.bind('right', () => this.navigateToDesign(this.nextDesign)); - }, - beforeDestroy() { - Mousetrap.unbind(['left', 'right'], this.navigateToDesign); - }, - methods: { - navigateToDesign(design) { - if (design) { - this.$router.push({ - name: DESIGN_ROUTE_NAME, - params: { id: design.filename }, - query: this.$route.query, - }); - } - }, - }, -}; -</script> - -<template> - <div v-if="designsCount" class="d-flex align-items-center"> - {{ paginationText }} - <div class="btn-group ml-3 mr-3"> - <pagination-button - :design="previousDesign" - :title="s__('DesignManagement|Go to previous design')" - icon-name="angle-left" - class="js-previous-design" - /> - <pagination-button - :design="nextDesign" - :title="s__('DesignManagement|Go to next design')" - icon-name="angle-right" - class="js-next-design" - /> - </div> - </div> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/toolbar/pagination_button.vue b/app/assets/javascripts/design_management_legacy/components/toolbar/pagination_button.vue deleted file mode 100644 index f00ecefca01..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/toolbar/pagination_button.vue +++ /dev/null @@ -1,48 +0,0 @@ -<script> -import Icon from '~/vue_shared/components/icon.vue'; -import { DESIGN_ROUTE_NAME } from '../../router/constants'; - -export default { - components: { - Icon, - }, - props: { - design: { - type: Object, - required: false, - default: null, - }, - title: { - type: String, - required: true, - }, - iconName: { - type: String, - required: true, - }, - }, - computed: { - designLink() { - if (!this.design) return {}; - - return { - name: DESIGN_ROUTE_NAME, - params: { id: this.design.filename }, - query: this.$route.query, - }; - }, - }, -}; -</script> - -<template> - <router-link - :to="designLink" - :disabled="!design" - :class="{ disabled: !design }" - :aria-label="title" - class="btn btn-default" - > - <icon :name="iconName" /> - </router-link> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/upload/button.vue b/app/assets/javascripts/design_management_legacy/components/upload/button.vue deleted file mode 100644 index 68555104a3c..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/upload/button.vue +++ /dev/null @@ -1,58 +0,0 @@ -<script> -import { GlDeprecatedButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; -import { VALID_DESIGN_FILE_MIMETYPE } from '../../constants'; - -export default { - components: { - GlDeprecatedButton, - GlLoadingIcon, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - props: { - isSaving: { - type: Boolean, - required: true, - }, - }, - methods: { - openFileUpload() { - this.$refs.fileUpload.click(); - }, - onFileUploadChange(e) { - this.$emit('upload', e.target.files); - }, - }, - VALID_DESIGN_FILE_MIMETYPE, -}; -</script> - -<template> - <div> - <gl-deprecated-button - v-gl-tooltip.hover - :title=" - s__( - 'DesignManagement|Adding a design with the same filename replaces the file in a new version.', - ) - " - :disabled="isSaving" - variant="success" - @click="openFileUpload" - > - {{ s__('DesignManagement|Upload designs') }} - <gl-loading-icon v-if="isSaving" inline class="ml-1" /> - </gl-deprecated-button> - - <input - ref="fileUpload" - type="file" - name="design_file" - :accept="$options.VALID_DESIGN_FILE_MIMETYPE.mimetype" - class="hide" - multiple - @change="onFileUploadChange" - /> - </div> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/upload/design_dropzone.vue b/app/assets/javascripts/design_management_legacy/components/upload/design_dropzone.vue deleted file mode 100644 index e435c84c959..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/upload/design_dropzone.vue +++ /dev/null @@ -1,134 +0,0 @@ -<script> -import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; -import uploadDesignMutation from '../../graphql/mutations/upload_design.mutation.graphql'; -import { UPLOAD_DESIGN_INVALID_FILETYPE_ERROR } from '../../utils/error_messages'; -import { isValidDesignFile } from '../../utils/design_management_utils'; -import { VALID_DATA_TRANSFER_TYPE, VALID_DESIGN_FILE_MIMETYPE } from '../../constants'; - -export default { - components: { - GlIcon, - GlLink, - GlSprintf, - }, - data() { - return { - dragCounter: 0, - isDragDataValid: false, - }; - }, - computed: { - dragging() { - return this.dragCounter !== 0; - }, - }, - methods: { - isValidUpload(files) { - return files.every(isValidDesignFile); - }, - isValidDragDataType({ dataTransfer }) { - return Boolean(dataTransfer && dataTransfer.types.some(t => t === VALID_DATA_TRANSFER_TYPE)); - }, - ondrop({ dataTransfer = {} }) { - this.dragCounter = 0; - // User already had feedback when dropzone was active, so bail here - if (!this.isDragDataValid) { - return; - } - - const { files } = dataTransfer; - if (!this.isValidUpload(Array.from(files))) { - createFlash(UPLOAD_DESIGN_INVALID_FILETYPE_ERROR); - return; - } - - this.$emit('change', files); - }, - ondragenter(e) { - this.dragCounter += 1; - this.isDragDataValid = this.isValidDragDataType(e); - }, - ondragleave() { - this.dragCounter -= 1; - }, - openFileUpload() { - this.$refs.fileUpload.click(); - }, - onDesignInputChange(e) { - this.$emit('change', e.target.files); - }, - }, - uploadDesignMutation, - VALID_DESIGN_FILE_MIMETYPE, -}; -</script> - -<template> - <div - class="w-100 position-relative" - @dragstart.prevent.stop - @dragend.prevent.stop - @dragover.prevent.stop - @dragenter.prevent.stop="ondragenter" - @dragleave.prevent.stop="ondragleave" - @drop.prevent.stop="ondrop" - > - <slot> - <button - class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3" - @click="openFileUpload" - > - <div class="d-flex-center flex-column text-center"> - <gl-icon name="doc-new" :size="48" class="mb-4" /> - <p> - <gl-sprintf - :message=" - __( - '%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}.', - ) - " - > - <template #lineOne="{ content }" - ><span class="d-block">{{ content }}</span> - </template> - - <template #link="{ content }"> - <gl-link class="h-100 w-100" @click.stop="openFileUpload">{{ content }}</gl-link> - </template> - </gl-sprintf> - </p> - </div> - </button> - - <input - ref="fileUpload" - type="file" - name="design_file" - :accept="$options.VALID_DESIGN_FILE_MIMETYPE.mimetype" - class="hide" - multiple - @change="onDesignInputChange" - /> - </slot> - <transition name="design-dropzone-fade"> - <div - v-show="dragging" - class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white" - > - <div v-show="!isDragDataValid" class="mw-50 text-center"> - <h3>{{ __('Oh no!') }}</h3> - <span>{{ - __( - 'You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.', - ) - }}</span> - </div> - <div v-show="isDragDataValid" class="mw-50 text-center"> - <h3>{{ __('Incoming!') }}</h3> - <span>{{ __('Drop your designs to start your upload.') }}</span> - </div> - </div> - </transition> - </div> -</template> diff --git a/app/assets/javascripts/design_management_legacy/components/upload/design_version_dropdown.vue b/app/assets/javascripts/design_management_legacy/components/upload/design_version_dropdown.vue deleted file mode 100644 index 879d2523848..00000000000 --- a/app/assets/javascripts/design_management_legacy/components/upload/design_version_dropdown.vue +++ /dev/null @@ -1,76 +0,0 @@ -<script> -import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui'; -import { __, sprintf } from '~/locale'; -import allVersionsMixin from '../../mixins/all_versions'; -import { findVersionId } from '../../utils/design_management_utils'; - -export default { - components: { - GlDeprecatedDropdown, - GlDeprecatedDropdownItem, - }, - mixins: [allVersionsMixin], - computed: { - queryVersion() { - return this.$route.query.version; - }, - currentVersionIdx() { - if (!this.queryVersion) return 0; - - const idx = this.allVersions.findIndex( - version => this.findVersionId(version.node.id) === this.queryVersion, - ); - - // if the currentVersionId isn't a valid version (i.e. not in allVersions) - // then return the latest version (index 0) - return idx !== -1 ? idx : 0; - }, - currentVersionId() { - if (this.queryVersion) return this.queryVersion; - - const currentVersion = this.allVersions[this.currentVersionIdx]; - return this.findVersionId(currentVersion.node.id); - }, - dropdownText() { - if (this.isLatestVersion) { - return __('Showing Latest Version'); - } - // allVersions is sorted in reverse chronological order (latest first) - const currentVersionNumber = this.allVersions.length - this.currentVersionIdx; - - return sprintf(__('Showing Version #%{versionNumber}'), { - versionNumber: currentVersionNumber, - }); - }, - }, - methods: { - findVersionId, - }, -}; -</script> - -<template> - <gl-deprecated-dropdown :text="dropdownText" variant="link" class="design-version-dropdown"> - <gl-deprecated-dropdown-item v-for="(version, index) in allVersions" :key="version.node.id"> - <router-link - class="d-flex js-version-link" - :to="{ path: $route.path, query: { version: findVersionId(version.node.id) } }" - > - <div class="flex-grow-1 ml-2"> - <div> - <strong - >{{ __('Version') }} {{ allVersions.length - index }} - <span v-if="findVersionId(version.node.id) === latestVersionId" - >({{ __('latest') }})</span - > - </strong> - </div> - </div> - <i - v-if="findVersionId(version.node.id) === currentVersionId" - class="fa fa-check float-right gl-mr-2" - ></i> - </router-link> - </gl-deprecated-dropdown-item> - </gl-deprecated-dropdown> -</template> diff --git a/app/assets/javascripts/design_management_legacy/constants.js b/app/assets/javascripts/design_management_legacy/constants.js deleted file mode 100644 index 21ff361a277..00000000000 --- a/app/assets/javascripts/design_management_legacy/constants.js +++ /dev/null @@ -1,16 +0,0 @@ -// WARNING: replace this with something -// more sensical as per https://gitlab.com/gitlab-org/gitlab/issues/118611 -export const VALID_DESIGN_FILE_MIMETYPE = { - mimetype: 'image/*', - regex: /image\/.+/, -}; - -// https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/types -export const VALID_DATA_TRANSFER_TYPE = 'Files'; - -export const ACTIVE_DISCUSSION_SOURCE_TYPES = { - pin: 'pin', - discussion: 'discussion', -}; - -export const DESIGN_DETAIL_LAYOUT_CLASSLIST = ['design-detail-layout', 'overflow-hidden', 'm-0']; diff --git a/app/assets/javascripts/design_management_legacy/graphql.js b/app/assets/javascripts/design_management_legacy/graphql.js deleted file mode 100644 index fae337aa75b..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql.js +++ /dev/null @@ -1,45 +0,0 @@ -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import { uniqueId } from 'lodash'; -import { defaultDataIdFromObject } from 'apollo-cache-inmemory'; -import createDefaultClient from '~/lib/graphql'; -import activeDiscussionQuery from './graphql/queries/active_discussion.query.graphql'; -import typeDefs from './graphql/typedefs.graphql'; - -Vue.use(VueApollo); - -const resolvers = { - Mutation: { - updateActiveDiscussion: (_, { id = null, source }, { cache }) => { - const data = cache.readQuery({ query: activeDiscussionQuery }); - data.activeDiscussion = { - __typename: 'ActiveDiscussion', - id, - source, - }; - cache.writeQuery({ query: activeDiscussionQuery, data }); - }, - }, -}; - -const defaultClient = createDefaultClient( - resolvers, - // This config is added temporarily to resolve an issue with duplicate design IDs. - // Should be removed as soon as https://gitlab.com/gitlab-org/gitlab/issues/13495 is resolved - { - cacheConfig: { - 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(); - } - return defaultDataIdFromObject(object); - }, - }, - typeDefs, - }, -); - -export default new VueApollo({ - defaultClient, -}); diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/design.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/design.fragment.graphql deleted file mode 100644 index 4b1703e41c3..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/fragments/design.fragment.graphql +++ /dev/null @@ -1,24 +0,0 @@ -#import "./design_note.fragment.graphql" -#import "./design_list.fragment.graphql" -#import "./diff_refs.fragment.graphql" -#import "./discussion_resolved_status.fragment.graphql" - -fragment DesignItem on Design { - ...DesignListItem - fullPath - diffRefs { - ...DesignDiffRefs - } - discussions { - nodes { - id - replyId - ...ResolvedStatus - notes { - nodes { - ...DesignNote - } - } - } - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/design_list.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/design_list.fragment.graphql deleted file mode 100644 index bc3132f9b42..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/fragments/design_list.fragment.graphql +++ /dev/null @@ -1,8 +0,0 @@ -fragment DesignListItem on Design { - id - event - filename - notesCount - image - imageV432x230 -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/design_note.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/design_note.fragment.graphql deleted file mode 100644 index 26edd2c0be1..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/fragments/design_note.fragment.graphql +++ /dev/null @@ -1,29 +0,0 @@ -#import "./diff_refs.fragment.graphql" -#import "~/graphql_shared/fragments/author.fragment.graphql" -#import "./note_permissions.fragment.graphql" - -fragment DesignNote on Note { - id - author { - ...Author - } - body - bodyHtml - createdAt - resolved - position { - diffRefs { - ...DesignDiffRefs - } - x - y - height - width - } - userPermissions { - ...DesignNotePermissions - } - discussion { - id - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/diff_refs.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/diff_refs.fragment.graphql deleted file mode 100644 index 984a55814b0..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/fragments/diff_refs.fragment.graphql +++ /dev/null @@ -1,5 +0,0 @@ -fragment DesignDiffRefs on DiffRefs { - baseSha - startSha - headSha -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/discussion_resolved_status.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/discussion_resolved_status.fragment.graphql deleted file mode 100644 index 7483b508721..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/fragments/discussion_resolved_status.fragment.graphql +++ /dev/null @@ -1,9 +0,0 @@ -fragment ResolvedStatus on Discussion { - resolvable - resolved - resolvedAt - resolvedBy { - name - webUrl - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/note_permissions.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/note_permissions.fragment.graphql deleted file mode 100644 index c243e39f3d3..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/fragments/note_permissions.fragment.graphql +++ /dev/null @@ -1,3 +0,0 @@ -fragment DesignNotePermissions on NotePermissions { - adminNote -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/fragments/version.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/version.fragment.graphql deleted file mode 100644 index 7eb40b12f51..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/fragments/version.fragment.graphql +++ /dev/null @@ -1,4 +0,0 @@ -fragment VersionListItem on DesignVersion { - id - sha -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/create_image_diff_note.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/create_image_diff_note.mutation.graphql deleted file mode 100644 index c8ade328120..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/mutations/create_image_diff_note.mutation.graphql +++ /dev/null @@ -1,21 +0,0 @@ -#import "../fragments/design_note.fragment.graphql" - -mutation createImageDiffNote($input: CreateImageDiffNoteInput!) { - createImageDiffNote(input: $input) { - note { - ...DesignNote - discussion { - id - replyId - notes { - edges { - node { - ...DesignNote - } - } - } - } - } - errors - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/create_note.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/create_note.mutation.graphql deleted file mode 100644 index 184ee6955dc..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/mutations/create_note.mutation.graphql +++ /dev/null @@ -1,10 +0,0 @@ -#import "../fragments/design_note.fragment.graphql" - -mutation createNote($input: CreateNoteInput!) { - createNote(input: $input) { - note { - ...DesignNote - } - errors - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/destroy_design.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/destroy_design.mutation.graphql deleted file mode 100644 index 0b3cf636cdb..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/mutations/destroy_design.mutation.graphql +++ /dev/null @@ -1,10 +0,0 @@ -#import "../fragments/version.fragment.graphql" - -mutation destroyDesign($filenames: [String!]!, $projectPath: ID!, $iid: ID!) { - designManagementDelete(input: { projectPath: $projectPath, iid: $iid, filenames: $filenames }) { - version { - ...VersionListItem - } - errors - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/toggle_resolve_discussion.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/toggle_resolve_discussion.mutation.graphql deleted file mode 100644 index 1157fc05d5f..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/mutations/toggle_resolve_discussion.mutation.graphql +++ /dev/null @@ -1,17 +0,0 @@ -#import "../fragments/design_note.fragment.graphql" -#import "../fragments/discussion_resolved_status.fragment.graphql" - -mutation toggleResolveDiscussion($id: ID!, $resolve: Boolean!) { - discussionToggleResolve(input: { id: $id, resolve: $resolve }) { - discussion { - id - ...ResolvedStatus - notes { - nodes { - ...DesignNote - } - } - } - errors - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql deleted file mode 100644 index a24b6737159..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql +++ /dev/null @@ -1,3 +0,0 @@ -mutation updateActiveDiscussion($id: String, $source: String) { - updateActiveDiscussion(id: $id, source: $source) @client -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_image_diff_note.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/update_image_diff_note.mutation.graphql deleted file mode 100644 index 5562ca9d89f..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_image_diff_note.mutation.graphql +++ /dev/null @@ -1,10 +0,0 @@ -#import "../fragments/design_note.fragment.graphql" - -mutation updateImageDiffNote($input: UpdateImageDiffNoteInput!) { - updateImageDiffNote(input: $input) { - errors - note { - ...DesignNote - } - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_note.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/update_note.mutation.graphql deleted file mode 100644 index b995e99fb6a..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/mutations/update_note.mutation.graphql +++ /dev/null @@ -1,10 +0,0 @@ -#import "../fragments/design_note.fragment.graphql" - -mutation updateNote($input: UpdateNoteInput!) { - updateNote(input: $input) { - note { - ...DesignNote - } - errors - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/mutations/upload_design.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/upload_design.mutation.graphql deleted file mode 100644 index d694e6558a0..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/mutations/upload_design.mutation.graphql +++ /dev/null @@ -1,21 +0,0 @@ -#import "../fragments/design.fragment.graphql" - -mutation uploadDesign($files: [Upload!]!, $projectPath: ID!, $iid: ID!) { - designManagementUpload(input: { projectPath: $projectPath, iid: $iid, files: $files }) { - designs { - ...DesignItem - versions { - edges { - node { - id - sha - } - } - } - } - skippedDesigns { - filename - } - errors - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/queries/active_discussion.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/active_discussion.query.graphql deleted file mode 100644 index 111023cea68..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/queries/active_discussion.query.graphql +++ /dev/null @@ -1,6 +0,0 @@ -query activeDiscussion { - activeDiscussion @client { - id - source - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/queries/app_data.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/app_data.query.graphql deleted file mode 100644 index e1269761206..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/queries/app_data.query.graphql +++ /dev/null @@ -1,4 +0,0 @@ -query projectFullPath { - projectPath @client - issueIid @client -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/queries/design_permissions.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/design_permissions.query.graphql deleted file mode 100644 index a87b256dc95..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/queries/design_permissions.query.graphql +++ /dev/null @@ -1,10 +0,0 @@ -query permissions($fullPath: ID!, $iid: String!) { - project(fullPath: $fullPath) { - id - issue(iid: $iid) { - userPermissions { - createDesign - } - } - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/queries/get_design.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/get_design.query.graphql deleted file mode 100644 index 07a9af55787..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/queries/get_design.query.graphql +++ /dev/null @@ -1,31 +0,0 @@ -#import "../fragments/design.fragment.graphql" -#import "~/graphql_shared/fragments/author.fragment.graphql" - -query getDesign($fullPath: ID!, $iid: String!, $atVersion: ID, $filenames: [String!]) { - project(fullPath: $fullPath) { - id - issue(iid: $iid) { - designCollection { - designs(atVersion: $atVersion, filenames: $filenames) { - edges { - node { - ...DesignItem - issue { - title - webPath - webUrl - participants { - edges { - node { - ...Author - } - } - } - } - } - } - } - } - } - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/queries/get_design_list.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/get_design_list.query.graphql deleted file mode 100644 index 121a50555b3..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/queries/get_design_list.query.graphql +++ /dev/null @@ -1,26 +0,0 @@ -#import "../fragments/design_list.fragment.graphql" -#import "../fragments/version.fragment.graphql" - -query getDesignList($fullPath: ID!, $iid: String!, $atVersion: ID) { - project(fullPath: $fullPath) { - id - issue(iid: $iid) { - designCollection { - designs(atVersion: $atVersion) { - edges { - node { - ...DesignListItem - } - } - } - versions { - edges { - node { - ...VersionListItem - } - } - } - } - } - } -} diff --git a/app/assets/javascripts/design_management_legacy/graphql/typedefs.graphql b/app/assets/javascripts/design_management_legacy/graphql/typedefs.graphql deleted file mode 100644 index fdbad4a90e0..00000000000 --- a/app/assets/javascripts/design_management_legacy/graphql/typedefs.graphql +++ /dev/null @@ -1,12 +0,0 @@ -type ActiveDiscussion { - id: ID - source: String -} - -extend type Query { - activeDiscussion: ActiveDiscussion -} - -extend type Mutation { - updateActiveDiscussion(id: ID!, source: String!): Boolean -} diff --git a/app/assets/javascripts/design_management_legacy/index.js b/app/assets/javascripts/design_management_legacy/index.js deleted file mode 100644 index 1fc5779515a..00000000000 --- a/app/assets/javascripts/design_management_legacy/index.js +++ /dev/null @@ -1,61 +0,0 @@ -// This application is being moved, please do not touch this files -// Please see https://gitlab.com/gitlab-org/gitlab/-/issues/14744#note_364468096 for details - -import $ from 'jquery'; -import Vue from 'vue'; -import createRouter from './router'; -import App from './components/app.vue'; -import apolloProvider from './graphql'; -import getDesignListQuery from './graphql/queries/get_design_list.query.graphql'; -import { DESIGNS_ROUTE_NAME, ROOT_ROUTE_NAME } from './router/constants'; - -export default () => { - const el = document.querySelector('.js-design-management'); - const badge = document.querySelector('.js-designs-count'); - const { issueIid, projectPath, issuePath } = el.dataset; - const router = createRouter(issuePath); - - $('.js-issue-tabs').on('shown.bs.tab', ({ target: { id } }) => { - if (id === 'designs' && router.currentRoute.name === ROOT_ROUTE_NAME) { - router.push({ name: DESIGNS_ROUTE_NAME }); - } else if (id === 'discussion') { - router.push({ name: ROOT_ROUTE_NAME }); - } - }); - - apolloProvider.clients.defaultClient.cache.writeData({ - data: { - projectPath, - issueIid, - activeDiscussion: { - __typename: 'ActiveDiscussion', - id: null, - source: null, - }, - }, - }); - - apolloProvider.clients.defaultClient - .watchQuery({ - query: getDesignListQuery, - variables: { - fullPath: projectPath, - iid: issueIid, - atVersion: null, - }, - }) - .subscribe(({ data }) => { - if (badge) { - badge.textContent = data.project.issue.designCollection.designs.edges.length; - } - }); - - return new Vue({ - el, - router, - apolloProvider, - render(createElement) { - return createElement(App); - }, - }); -}; diff --git a/app/assets/javascripts/design_management_legacy/mixins/all_designs.js b/app/assets/javascripts/design_management_legacy/mixins/all_designs.js deleted file mode 100644 index 544429928d2..00000000000 --- a/app/assets/javascripts/design_management_legacy/mixins/all_designs.js +++ /dev/null @@ -1,49 +0,0 @@ -import { propertyOf } from 'lodash'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; -import { s__ } from '~/locale'; -import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql'; -import { extractNodes } from '../utils/design_management_utils'; -import allVersionsMixin from './all_versions'; -import { DESIGNS_ROUTE_NAME } from '../router/constants'; - -export default { - mixins: [allVersionsMixin], - apollo: { - designs: { - query: getDesignListQuery, - variables() { - return { - fullPath: this.projectPath, - iid: this.issueIid, - atVersion: this.designsVersion, - }; - }, - update: data => { - const designEdges = propertyOf(data)(['project', 'issue', 'designCollection', 'designs']); - if (designEdges) { - return extractNodes(designEdges); - } - return []; - }, - error() { - this.error = true; - }, - result() { - if (this.$route.query.version && !this.hasValidVersion) { - createFlash( - s__( - 'DesignManagement|Requested design version does not exist. Showing latest version instead', - ), - ); - this.$router.replace({ name: DESIGNS_ROUTE_NAME, query: { version: undefined } }); - } - }, - }, - }, - data() { - return { - designs: [], - error: false, - }; - }, -}; diff --git a/app/assets/javascripts/design_management_legacy/mixins/all_versions.js b/app/assets/javascripts/design_management_legacy/mixins/all_versions.js deleted file mode 100644 index 3966fe71732..00000000000 --- a/app/assets/javascripts/design_management_legacy/mixins/all_versions.js +++ /dev/null @@ -1,62 +0,0 @@ -import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql'; -import appDataQuery from '../graphql/queries/app_data.query.graphql'; -import { findVersionId } from '../utils/design_management_utils'; - -export default { - apollo: { - appData: { - query: appDataQuery, - manual: true, - result({ data: { projectPath, issueIid } }) { - this.projectPath = projectPath; - this.issueIid = issueIid; - }, - }, - allVersions: { - query: getDesignListQuery, - variables() { - return { - fullPath: this.projectPath, - iid: this.issueIid, - atVersion: null, - }; - }, - update: data => data.project.issue.designCollection.versions.edges, - }, - }, - computed: { - hasValidVersion() { - return ( - this.$route.query.version && - this.allVersions && - this.allVersions.some(version => version.node.id.endsWith(this.$route.query.version)) - ); - }, - designsVersion() { - return this.hasValidVersion - ? `gid://gitlab/DesignManagement::Version/${this.$route.query.version}` - : null; - }, - latestVersionId() { - const latestVersion = this.allVersions[0]; - return latestVersion && findVersionId(latestVersion.node.id); - }, - isLatestVersion() { - if (this.allVersions.length > 0) { - return ( - !this.$route.query.version || - !this.latestVersionId || - this.$route.query.version === this.latestVersionId - ); - } - return true; - }, - }, - data() { - return { - allVersions: [], - projectPath: '', - issueIid: null, - }; - }, -}; diff --git a/app/assets/javascripts/design_management_legacy/pages/design/index.vue b/app/assets/javascripts/design_management_legacy/pages/design/index.vue deleted file mode 100644 index 2ada9eff8c6..00000000000 --- a/app/assets/javascripts/design_management_legacy/pages/design/index.vue +++ /dev/null @@ -1,378 +0,0 @@ -<script> -import Mousetrap from 'mousetrap'; -import { GlLoadingIcon, GlAlert } from '@gitlab/ui'; -import { ApolloMutation } from 'vue-apollo'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; -import { fetchPolicies } from '~/lib/graphql'; -import allVersionsMixin from '../../mixins/all_versions'; -import Toolbar from '../../components/toolbar/index.vue'; -import DesignDestroyer from '../../components/design_destroyer.vue'; -import DesignScaler from '../../components/design_scaler.vue'; -import DesignPresentation from '../../components/design_presentation.vue'; -import DesignReplyForm from '../../components/design_notes/design_reply_form.vue'; -import DesignSidebar from '../../components/design_sidebar.vue'; -import getDesignQuery from '../../graphql/queries/get_design.query.graphql'; -import appDataQuery from '../../graphql/queries/app_data.query.graphql'; -import createImageDiffNoteMutation from '../../graphql/mutations/create_image_diff_note.mutation.graphql'; -import updateImageDiffNoteMutation from '../../graphql/mutations/update_image_diff_note.mutation.graphql'; -import updateActiveDiscussionMutation from '../../graphql/mutations/update_active_discussion.mutation.graphql'; -import { - extractDiscussions, - extractDesign, - updateImageDiffNoteOptimisticResponse, -} from '../../utils/design_management_utils'; -import { - updateStoreAfterAddImageDiffNote, - updateStoreAfterUpdateImageDiffNote, -} from '../../utils/cache_update'; -import { - ADD_DISCUSSION_COMMENT_ERROR, - ADD_IMAGE_DIFF_NOTE_ERROR, - UPDATE_IMAGE_DIFF_NOTE_ERROR, - DESIGN_NOT_FOUND_ERROR, - DESIGN_VERSION_NOT_EXIST_ERROR, - UPDATE_NOTE_ERROR, - designDeletionError, -} from '../../utils/error_messages'; -import { trackDesignDetailView } from '../../utils/tracking'; -import { DESIGNS_ROUTE_NAME } from '../../router/constants'; -import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants'; - -export default { - components: { - ApolloMutation, - DesignReplyForm, - DesignPresentation, - DesignScaler, - DesignDestroyer, - Toolbar, - GlLoadingIcon, - GlAlert, - DesignSidebar, - }, - mixins: [allVersionsMixin], - props: { - id: { - type: String, - required: true, - }, - }, - data() { - return { - design: {}, - comment: '', - annotationCoordinates: null, - projectPath: '', - errorMessage: '', - issueIid: '', - scale: 1, - resolvedDiscussionsExpanded: false, - }; - }, - apollo: { - appData: { - query: appDataQuery, - manual: true, - result({ data: { projectPath, issueIid } }) { - this.projectPath = projectPath; - this.issueIid = issueIid; - }, - }, - design: { - query: getDesignQuery, - // We want to see cached design version if we have one, and fetch newer version on the background to update discussions - fetchPolicy: fetchPolicies.CACHE_AND_NETWORK, - variables() { - return this.designVariables; - }, - update: data => extractDesign(data), - result(res) { - this.onDesignQueryResult(res); - }, - error() { - this.onQueryError(DESIGN_NOT_FOUND_ERROR); - }, - }, - }, - computed: { - isFirstLoading() { - // We only want to show spinner on initial design load (when opened from a deep link to design) - // If we already have cached a design, loading shouldn't be indicated to user - return this.$apollo.queries.design.loading && !this.design.filename; - }, - discussions() { - if (!this.design.discussions) { - return []; - } - return extractDiscussions(this.design.discussions); - }, - markdownPreviewPath() { - return `/${this.projectPath}/preview_markdown?target_type=Issue`; - }, - isSubmitButtonDisabled() { - return this.comment.trim().length === 0; - }, - designVariables() { - return { - fullPath: this.projectPath, - iid: this.issueIid, - filenames: [this.$route.params.id], - atVersion: this.designsVersion, - }; - }, - mutationPayload() { - const { x, y, width, height } = this.annotationCoordinates; - return { - noteableId: this.design.id, - body: this.comment, - position: { - headSha: this.design.diffRefs.headSha, - baseSha: this.design.diffRefs.baseSha, - startSha: this.design.diffRefs.startSha, - x, - y, - width, - height, - paths: { - newPath: this.design.fullPath, - }, - }, - }; - }, - isAnnotating() { - return Boolean(this.annotationCoordinates); - }, - resolvedDiscussions() { - return this.discussions.filter(discussion => discussion.resolved); - }, - }, - watch: { - resolvedDiscussions(val) { - if (!val.length) { - this.resolvedDiscussionsExpanded = false; - } - }, - }, - mounted() { - Mousetrap.bind('esc', this.closeDesign); - this.trackEvent(); - // We need to reset the active discussion when opening a new design - this.updateActiveDiscussion(); - }, - beforeDestroy() { - Mousetrap.unbind('esc', this.closeDesign); - }, - methods: { - addImageDiffNoteToStore( - store, - { - data: { createImageDiffNote }, - }, - ) { - updateStoreAfterAddImageDiffNote( - store, - createImageDiffNote, - getDesignQuery, - this.designVariables, - ); - }, - updateImageDiffNoteInStore( - store, - { - data: { updateImageDiffNote }, - }, - ) { - return updateStoreAfterUpdateImageDiffNote( - store, - updateImageDiffNote, - getDesignQuery, - this.designVariables, - ); - }, - onMoveNote({ noteId, discussionId, position }) { - const discussion = this.discussions.find(({ id }) => id === discussionId); - const note = discussion.notes.find( - ({ discussion: noteDiscussion }) => noteDiscussion.id === discussionId, - ); - - const mutationPayload = { - optimisticResponse: updateImageDiffNoteOptimisticResponse(note, { - position, - }), - variables: { - input: { - id: noteId, - position, - }, - }, - mutation: updateImageDiffNoteMutation, - update: this.updateImageDiffNoteInStore, - }; - - 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 - // To prevent throwing an error, we don't perform any logic until loading is false - if (loading) { - return; - } - - if (!data || !extractDesign(data)) { - this.onQueryError(DESIGN_NOT_FOUND_ERROR); - } else if (this.$route.query.version && !this.hasValidVersion) { - this.onQueryError(DESIGN_VERSION_NOT_EXIST_ERROR); - } - }, - onQueryError(message) { - // because we redirect user to /designs (the issue page), - // we want to create these flashes on the issue page - createFlash(message); - this.$router.push({ name: this.$options.DESIGNS_ROUTE_NAME }); - }, - onError(message, e) { - this.errorMessage = message; - throw e; - }, - onCreateImageDiffNoteError(e) { - this.onError(ADD_IMAGE_DIFF_NOTE_ERROR, e); - }, - onUpdateNoteError(e) { - this.onError(UPDATE_NOTE_ERROR, e); - }, - onDesignDiscussionError(e) { - this.onError(ADD_DISCUSSION_COMMENT_ERROR, e); - }, - onUpdateImageDiffNoteError(e) { - this.onError(UPDATE_IMAGE_DIFF_NOTE_ERROR, e); - }, - onDesignDeleteError(e) { - this.onError(designDeletionError({ singular: true }), e); - }, - onResolveDiscussionError(e) { - this.onError(UPDATE_IMAGE_DIFF_NOTE_ERROR, e); - }, - openCommentForm(annotationCoordinates) { - this.annotationCoordinates = annotationCoordinates; - if (this.$refs.newDiscussionForm) { - this.$refs.newDiscussionForm.focusInput(); - } - }, - closeCommentForm() { - this.comment = ''; - this.annotationCoordinates = null; - }, - closeDesign() { - this.$router.push({ - name: this.$options.DESIGNS_ROUTE_NAME, - query: this.$route.query, - }); - }, - trackEvent() { - // TODO: This needs to be made aware of referers, or if it's rendered in a different context than a Issue - trackDesignDetailView( - 'issue-design-collection', - 'issue', - this.$route.query.version || this.latestVersionId, - this.isLatestVersion, - ); - }, - updateActiveDiscussion(id) { - this.$apollo.mutate({ - mutation: updateActiveDiscussionMutation, - variables: { - id, - source: ACTIVE_DISCUSSION_SOURCE_TYPES.discussion, - }, - }); - }, - toggleResolvedComments() { - this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded; - }, - }, - createImageDiffNoteMutation, - DESIGNS_ROUTE_NAME, -}; -</script> - -<template> - <div - class="design-detail js-design-detail fixed-top w-100 position-bottom-0 d-flex justify-content-center flex-column flex-lg-row" - > - <gl-loading-icon v-if="isFirstLoading" size="xl" class="align-self-center" /> - <template v-else> - <div class="d-flex overflow-hidden flex-grow-1 flex-column position-relative"> - <design-destroyer - :filenames="[design.filename]" - :project-path="projectPath" - :iid="issueIid" - @done="$router.push({ name: $options.DESIGNS_ROUTE_NAME })" - @error="onDesignDeleteError" - > - <template #default="{ mutate, loading }"> - <toolbar - :id="id" - :is-deleting="loading" - :is-latest-version="isLatestVersion" - v-bind="design" - @delete="mutate" - /> - </template> - </design-destroyer> - - <div v-if="errorMessage" class="p-3"> - <gl-alert variant="danger" @dismiss="errorMessage = null"> - {{ errorMessage }} - </gl-alert> - </div> - <design-presentation - :image="design.image" - :image-name="design.filename" - :discussions="discussions" - :is-annotating="isAnnotating" - :scale="scale" - :resolved-discussions-expanded="resolvedDiscussionsExpanded" - @openCommentForm="openCommentForm" - @closeCommentForm="closeCommentForm" - @moveNote="onMoveNote" - /> - - <div class="design-scaler-wrapper position-absolute mb-4 d-flex-center"> - <design-scaler @scale="scale = $event" /> - </div> - </div> - <design-sidebar - :design="design" - :resolved-discussions-expanded="resolvedDiscussionsExpanded" - :markdown-preview-path="markdownPreviewPath" - @onDesignDiscussionError="onDesignDiscussionError" - @onCreateImageDiffNoteError="onCreateImageDiffNoteError" - @updateNoteError="onUpdateNoteError" - @resolveDiscussionError="onResolveDiscussionError" - @toggleResolvedComments="toggleResolvedComments" - > - <template #replyForm> - <apollo-mutation - v-if="isAnnotating" - #default="{ mutate, loading }" - :mutation="$options.createImageDiffNoteMutation" - :variables="{ - input: mutationPayload, - }" - :update="addImageDiffNoteToStore" - @done="closeCommentForm" - @error="onCreateImageDiffNoteError" - > - <design-reply-form - ref="newDiscussionForm" - v-model="comment" - :is-saving="loading" - :markdown-preview-path="markdownPreviewPath" - @submitForm="mutate" - @cancelForm="closeCommentForm" - /> </apollo-mutation - ></template> - </design-sidebar> - </template> - </div> -</template> diff --git a/app/assets/javascripts/design_management_legacy/pages/index.vue b/app/assets/javascripts/design_management_legacy/pages/index.vue deleted file mode 100644 index 66008a193ce..00000000000 --- a/app/assets/javascripts/design_management_legacy/pages/index.vue +++ /dev/null @@ -1,323 +0,0 @@ -<script> -import { GlLoadingIcon, GlDeprecatedButton, GlAlert } from '@gitlab/ui'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; -import { s__, sprintf } from '~/locale'; -import UploadButton from '../components/upload/button.vue'; -import DeleteButton from '../components/delete_button.vue'; -import Design from '../components/list/item.vue'; -import DesignDestroyer from '../components/design_destroyer.vue'; -import DesignVersionDropdown from '../components/upload/design_version_dropdown.vue'; -import DesignDropzone from '../components/upload/design_dropzone.vue'; -import uploadDesignMutation from '../graphql/mutations/upload_design.mutation.graphql'; -import permissionsQuery from '../graphql/queries/design_permissions.query.graphql'; -import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql'; -import allDesignsMixin from '../mixins/all_designs'; -import { - UPLOAD_DESIGN_ERROR, - EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE, - EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE, - designUploadSkippedWarning, - designDeletionError, -} from '../utils/error_messages'; -import { updateStoreAfterUploadDesign } from '../utils/cache_update'; -import { - designUploadOptimisticResponse, - isValidDesignFile, -} from '../utils/design_management_utils'; -import { getFilename } from '~/lib/utils/file_upload'; -import { DESIGNS_ROUTE_NAME } from '../router/constants'; - -const MAXIMUM_FILE_UPLOAD_LIMIT = 10; - -export default { - components: { - GlLoadingIcon, - GlAlert, - GlDeprecatedButton, - UploadButton, - Design, - DesignDestroyer, - DesignVersionDropdown, - DeleteButton, - DesignDropzone, - }, - mixins: [allDesignsMixin], - apollo: { - permissions: { - query: permissionsQuery, - variables() { - return { - fullPath: this.projectPath, - iid: this.issueIid, - }; - }, - update: data => data.project.issue.userPermissions, - }, - }, - data() { - return { - permissions: { - createDesign: false, - }, - filesToBeSaved: [], - selectedDesigns: [], - }; - }, - computed: { - isLoading() { - return this.$apollo.queries.designs.loading || this.$apollo.queries.permissions.loading; - }, - isSaving() { - return this.filesToBeSaved.length > 0; - }, - canCreateDesign() { - return this.permissions.createDesign; - }, - showToolbar() { - return this.canCreateDesign && this.allVersions.length > 0; - }, - hasDesigns() { - return this.designs.length > 0; - }, - hasSelectedDesigns() { - return this.selectedDesigns.length > 0; - }, - canDeleteDesigns() { - return this.isLatestVersion && this.hasSelectedDesigns; - }, - projectQueryBody() { - return { - query: getDesignListQuery, - variables: { fullPath: this.projectPath, iid: this.issueIid, atVersion: null }, - }; - }, - selectAllButtonText() { - return this.hasSelectedDesigns - ? s__('DesignManagement|Deselect all') - : s__('DesignManagement|Select all'); - }, - }, - mounted() { - this.toggleOnPasteListener(this.$route.name); - }, - methods: { - resetFilesToBeSaved() { - this.filesToBeSaved = []; - }, - /** - * Determine if a design upload is valid, given [files] - * @param {Array<File>} files - */ - isValidDesignUpload(files) { - if (!this.canCreateDesign) return false; - - if (files.length > MAXIMUM_FILE_UPLOAD_LIMIT) { - createFlash( - sprintf( - s__( - 'DesignManagement|The maximum number of designs allowed to be uploaded is %{upload_limit}. Please try again.', - ), - { - upload_limit: MAXIMUM_FILE_UPLOAD_LIMIT, - }, - ), - ); - - return false; - } - return true; - }, - onUploadDesign(files) { - // convert to Array so that we have Array methods (.map, .some, etc.) - this.filesToBeSaved = Array.from(files); - if (!this.isValidDesignUpload(this.filesToBeSaved)) return null; - - const mutationPayload = { - optimisticResponse: designUploadOptimisticResponse(this.filesToBeSaved), - variables: { - files: this.filesToBeSaved, - projectPath: this.projectPath, - iid: this.issueIid, - }, - context: { - hasUpload: true, - }, - mutation: uploadDesignMutation, - update: this.afterUploadDesign, - }; - - return this.$apollo - .mutate(mutationPayload) - .then(res => this.onUploadDesignDone(res)) - .catch(() => this.onUploadDesignError()); - }, - afterUploadDesign( - store, - { - data: { designManagementUpload }, - }, - ) { - updateStoreAfterUploadDesign(store, designManagementUpload, this.projectQueryBody); - }, - onUploadDesignDone(res) { - const skippedFiles = res?.data?.designManagementUpload?.skippedDesigns || []; - const skippedWarningMessage = designUploadSkippedWarning(this.filesToBeSaved, skippedFiles); - if (skippedWarningMessage) { - createFlash(skippedWarningMessage, 'warning'); - } - - // if this upload resulted in a new version being created, redirect user to the latest version - if (!this.isLatestVersion) { - this.$router.push({ name: DESIGNS_ROUTE_NAME }); - } - this.resetFilesToBeSaved(); - }, - onUploadDesignError() { - this.resetFilesToBeSaved(); - createFlash(UPLOAD_DESIGN_ERROR); - }, - changeSelectedDesigns(filename) { - if (this.isDesignSelected(filename)) { - this.selectedDesigns = this.selectedDesigns.filter(design => design !== filename); - } else { - this.selectedDesigns.push(filename); - } - }, - toggleDesignsSelection() { - if (this.hasSelectedDesigns) { - this.selectedDesigns = []; - } else { - 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); - }, - canSelectDesign(filename) { - return this.isLatestVersion && this.canCreateDesign && !this.isDesignToBeSaved(filename); - }, - onDesignDelete() { - this.selectedDesigns = []; - if (this.$route.query.version) this.$router.push({ name: DESIGNS_ROUTE_NAME }); - }, - onDesignDeleteError() { - const errorMessage = designDeletionError({ singular: this.selectedDesigns.length === 1 }); - createFlash(errorMessage); - }, - onExistingDesignDropzoneChange(files, existingDesignFilename) { - const filesArr = Array.from(files); - - if (filesArr.length > 1) { - createFlash(EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE); - return; - } - - if (!filesArr.some(({ name }) => existingDesignFilename === name)) { - createFlash(EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE); - return; - } - - this.onUploadDesign(files); - }, - onDesignPaste(event) { - const { clipboardData } = event; - const files = Array.from(clipboardData.files); - if (clipboardData && files.length > 0) { - if (!files.some(isValidDesignFile)) { - return; - } - event.preventDefault(); - let filename = getFilename(event); - if (!filename || filename === 'image.png') { - filename = `design_${Date.now()}.png`; - } - const newFile = new File([files[0]], filename); - this.onUploadDesign([newFile]); - } - }, - toggleOnPasteListener(route) { - if (route === DESIGNS_ROUTE_NAME) { - document.addEventListener('paste', this.onDesignPaste); - } else { - document.removeEventListener('paste', this.onDesignPaste); - } - }, - }, - beforeRouteUpdate(to, from, next) { - this.toggleOnPasteListener(to.name); - this.selectedDesigns = []; - next(); - }, - beforeRouteLeave(to, from, next) { - this.toggleOnPasteListener(to.name); - next(); - }, -}; -</script> - -<template> - <div> - <header v-if="showToolbar" class="row-content-block border-top-0 p-2 d-flex"> - <div class="d-flex justify-content-between align-items-center w-100"> - <design-version-dropdown /> - <div :class="['qa-selector-toolbar', { 'd-flex': hasDesigns, 'd-none': !hasDesigns }]"> - <gl-deprecated-button - v-if="isLatestVersion" - variant="link" - class="mr-2 js-select-all" - @click="toggleDesignsSelection" - >{{ selectAllButtonText }}</gl-deprecated-button - > - <design-destroyer - #default="{ mutate, loading }" - :filenames="selectedDesigns" - :project-path="projectPath" - :iid="issueIid" - @done="onDesignDelete" - @error="onDesignDeleteError" - > - <delete-button - v-if="isLatestVersion" - :is-deleting="loading" - button-class="btn-danger btn-inverted mr-2" - :has-selected-designs="hasSelectedDesigns" - @deleteSelectedDesigns="mutate()" - > - {{ s__('DesignManagement|Delete selected') }} - <gl-loading-icon v-if="loading" inline class="ml-1" /> - </delete-button> - </design-destroyer> - <upload-button v-if="canCreateDesign" :is-saving="isSaving" @upload="onUploadDesign" /> - </div> - </div> - </header> - <div class="mt-4"> - <gl-loading-icon v-if="isLoading" size="md" /> - <gl-alert v-else-if="error" variant="danger" :dismissible="false"> - {{ __('An error occurred while loading designs. Please try again.') }} - </gl-alert> - <ol v-else class="list-unstyled row"> - <li class="col-md-6 col-lg-4 mb-3"> - <design-dropzone class="design-list-item" @change="onUploadDesign" /> - </li> - <li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-4 mb-3"> - <design-dropzone @change="onExistingDesignDropzoneChange($event, design.filename)" - ><design v-bind="design" :is-uploading="isDesignToBeSaved(design.filename)" - /></design-dropzone> - - <input - v-if="canSelectDesign(design.filename)" - :checked="isDesignSelected(design.filename)" - type="checkbox" - class="design-checkbox" - @change="changeSelectedDesigns(design.filename)" - /> - </li> - </ol> - </div> - <router-view :key="$route.fullPath" /> - </div> -</template> diff --git a/app/assets/javascripts/design_management_legacy/router/constants.js b/app/assets/javascripts/design_management_legacy/router/constants.js deleted file mode 100644 index abeef520e33..00000000000 --- a/app/assets/javascripts/design_management_legacy/router/constants.js +++ /dev/null @@ -1,3 +0,0 @@ -export const ROOT_ROUTE_NAME = 'root'; -export const DESIGNS_ROUTE_NAME = 'designs'; -export const DESIGN_ROUTE_NAME = 'design'; diff --git a/app/assets/javascripts/design_management_legacy/router/index.js b/app/assets/javascripts/design_management_legacy/router/index.js deleted file mode 100644 index 28a81ed0278..00000000000 --- a/app/assets/javascripts/design_management_legacy/router/index.js +++ /dev/null @@ -1,35 +0,0 @@ -import $ from 'jquery'; -import Vue from 'vue'; -import VueRouter from 'vue-router'; -import routes from './routes'; -import { DESIGN_ROUTE_NAME } from './constants'; -import { getPageLayoutElement } from '~/design_management_legacy/utils/design_management_utils'; -import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '../constants'; - -Vue.use(VueRouter); - -export default function createRouter(base) { - const router = new VueRouter({ - base, - mode: 'history', - routes, - }); - const pageEl = getPageLayoutElement(); - - router.beforeEach(({ meta: { el }, name }, _, next) => { - $(`#${el}`).tab('show'); - - // apply a fullscreen layout style in Design View (a.k.a design detail) - if (pageEl) { - if (name === DESIGN_ROUTE_NAME) { - pageEl.classList.add(...DESIGN_DETAIL_LAYOUT_CLASSLIST); - } else { - pageEl.classList.remove(...DESIGN_DETAIL_LAYOUT_CLASSLIST); - } - } - - next(); - }); - - return router; -} diff --git a/app/assets/javascripts/design_management_legacy/router/routes.js b/app/assets/javascripts/design_management_legacy/router/routes.js deleted file mode 100644 index 788910e5514..00000000000 --- a/app/assets/javascripts/design_management_legacy/router/routes.js +++ /dev/null @@ -1,44 +0,0 @@ -import Home from '../pages/index.vue'; -import DesignDetail from '../pages/design/index.vue'; -import { ROOT_ROUTE_NAME, DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants'; - -export default [ - { - name: ROOT_ROUTE_NAME, - path: '/', - component: Home, - meta: { - el: 'discussion', - }, - }, - { - name: DESIGNS_ROUTE_NAME, - path: '/designs', - component: Home, - meta: { - el: 'designs', - }, - children: [ - { - name: DESIGN_ROUTE_NAME, - path: ':id', - component: DesignDetail, - meta: { - el: 'designs', - }, - beforeEnter( - { - params: { id }, - }, - from, - next, - ) { - if (typeof id === 'string') { - next(); - } - }, - props: ({ params: { id } }) => ({ id }), - }, - ], - }, -]; diff --git a/app/assets/javascripts/design_management_legacy/utils/cache_update.js b/app/assets/javascripts/design_management_legacy/utils/cache_update.js deleted file mode 100644 index 5ba6f84c413..00000000000 --- a/app/assets/javascripts/design_management_legacy/utils/cache_update.js +++ /dev/null @@ -1,276 +0,0 @@ -/* eslint-disable @gitlab/require-i18n-strings */ - -import { deprecatedCreateFlash as createFlash } from '~/flash'; -import { extractCurrentDiscussion, extractDesign } from './design_management_utils'; -import { - ADD_IMAGE_DIFF_NOTE_ERROR, - UPDATE_IMAGE_DIFF_NOTE_ERROR, - ADD_DISCUSSION_COMMENT_ERROR, - designDeletionError, -} from './error_messages'; - -const deleteDesignsFromStore = (store, query, selectedDesigns) => { - const data = store.readQuery(query); - - const changedDesigns = data.project.issue.designCollection.designs.edges.filter( - ({ node }) => !selectedDesigns.includes(node.filename), - ); - data.project.issue.designCollection.designs.edges = [...changedDesigns]; - - store.writeQuery({ - ...query, - data, - }); -}; - -/** - * Adds a new version of designs to store - * - * @param {Object} store - * @param {Object} query - * @param {Object} version - */ -const addNewVersionToStore = (store, query, version) => { - if (!version) return; - - const data = store.readQuery(query); - const newEdge = { node: version, __typename: 'DesignVersionEdge' }; - - data.project.issue.designCollection.versions.edges = [ - newEdge, - ...data.project.issue.designCollection.versions.edges, - ]; - - store.writeQuery({ - ...query, - data, - }); -}; - -const addDiscussionCommentToStore = (store, createNote, query, queryVariables, discussionId) => { - const data = store.readQuery({ - query, - variables: queryVariables, - }); - - const design = extractDesign(data); - const currentDiscussion = extractCurrentDiscussion(design.discussions, discussionId); - currentDiscussion.notes.nodes = [...currentDiscussion.notes.nodes, createNote.note]; - - design.notesCount += 1; - if ( - !design.issue.participants.edges.some( - participant => participant.node.username === createNote.note.author.username, - ) - ) { - design.issue.participants.edges = [ - ...design.issue.participants.edges, - { - __typename: 'UserEdge', - node: { - __typename: 'User', - ...createNote.note.author, - }, - }, - ]; - } - store.writeQuery({ - query, - variables: queryVariables, - data: { - ...data, - design: { - ...design, - }, - }, - }); -}; - -const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) => { - const data = store.readQuery({ - query, - variables, - }); - const newDiscussion = { - __typename: 'Discussion', - id: createImageDiffNote.note.discussion.id, - replyId: createImageDiffNote.note.discussion.replyId, - resolvable: true, - resolved: false, - resolvedAt: null, - resolvedBy: null, - notes: { - __typename: 'NoteConnection', - nodes: [createImageDiffNote.note], - }, - }; - const design = extractDesign(data); - const notesCount = design.notesCount + 1; - design.discussions.nodes = [...design.discussions.nodes, newDiscussion]; - if ( - !design.issue.participants.edges.some( - participant => participant.node.username === createImageDiffNote.note.author.username, - ) - ) { - design.issue.participants.edges = [ - ...design.issue.participants.edges, - { - __typename: 'UserEdge', - node: { - __typename: 'User', - ...createImageDiffNote.note.author, - }, - }, - ]; - } - store.writeQuery({ - query, - variables, - data: { - ...data, - design: { - ...design, - notesCount, - }, - }, - }); -}; - -const updateImageDiffNoteInStore = (store, updateImageDiffNote, query, variables) => { - const data = store.readQuery({ - query, - variables, - }); - - const design = extractDesign(data); - const discussion = extractCurrentDiscussion( - design.discussions, - updateImageDiffNote.note.discussion.id, - ); - - discussion.notes = { - ...discussion.notes, - nodes: [updateImageDiffNote.note, ...discussion.notes.nodes.slice(1)], - }; - - store.writeQuery({ - query, - variables, - data: { - ...data, - design, - }, - }); -}; - -const addNewDesignToStore = (store, designManagementUpload, query) => { - const data = store.readQuery(query); - - const newDesigns = data.project.issue.designCollection.designs.edges.reduce((acc, design) => { - if (!acc.find(d => d.filename === design.node.filename)) { - acc.push(design.node); - } - - return acc; - }, designManagementUpload.designs); - - let newVersionNode; - const findNewVersions = designManagementUpload.designs.find(design => design.versions); - - if (findNewVersions) { - const findNewVersionsEdges = findNewVersions.versions.edges; - - if (findNewVersionsEdges && findNewVersionsEdges.length) { - newVersionNode = [findNewVersionsEdges[0]]; - } - } - - const newVersions = [ - ...(newVersionNode || []), - ...data.project.issue.designCollection.versions.edges, - ]; - - const updatedDesigns = { - __typename: 'DesignCollection', - designs: { - __typename: 'DesignConnection', - edges: newDesigns.map(design => ({ - __typename: 'DesignEdge', - node: design, - })), - }, - versions: { - __typename: 'DesignVersionConnection', - edges: newVersions, - }, - }; - - data.project.issue.designCollection = updatedDesigns; - - store.writeQuery({ - ...query, - data, - }); -}; - -const onError = (data, message) => { - createFlash(message); - throw new Error(data.errors); -}; - -export const hasErrors = ({ errors = [] }) => errors?.length; - -/** - * Updates a store after design deletion - * - * @param {Object} store - * @param {Object} data - * @param {Object} query - * @param {Array} designs - */ -export const updateStoreAfterDesignsDelete = (store, data, query, designs) => { - if (hasErrors(data)) { - onError(data, designDeletionError({ singular: designs.length === 1 })); - } else { - deleteDesignsFromStore(store, query, designs); - addNewVersionToStore(store, query, data.version); - } -}; - -export const updateStoreAfterAddDiscussionComment = ( - store, - data, - query, - queryVariables, - discussionId, -) => { - if (hasErrors(data)) { - onError(data, ADD_DISCUSSION_COMMENT_ERROR); - } else { - addDiscussionCommentToStore(store, data, query, queryVariables, discussionId); - } -}; - -export const updateStoreAfterAddImageDiffNote = (store, data, query, queryVariables) => { - if (hasErrors(data)) { - onError(data, ADD_IMAGE_DIFF_NOTE_ERROR); - } else { - addImageDiffNoteToStore(store, data, query, queryVariables); - } -}; - -export const updateStoreAfterUpdateImageDiffNote = (store, data, query, queryVariables) => { - if (hasErrors(data)) { - onError(data, UPDATE_IMAGE_DIFF_NOTE_ERROR); - } else { - updateImageDiffNoteInStore(store, data, query, queryVariables); - } -}; - -export const updateStoreAfterUploadDesign = (store, data, query) => { - if (hasErrors(data)) { - onError(data, data.errors[0]); - } else { - addNewDesignToStore(store, data, query); - } -}; diff --git a/app/assets/javascripts/design_management_legacy/utils/design_management_utils.js b/app/assets/javascripts/design_management_legacy/utils/design_management_utils.js deleted file mode 100644 index 22705cf67a1..00000000000 --- a/app/assets/javascripts/design_management_legacy/utils/design_management_utils.js +++ /dev/null @@ -1,128 +0,0 @@ -import { uniqueId } from 'lodash'; -import { VALID_DESIGN_FILE_MIMETYPE } from '../constants'; - -export const isValidDesignFile = ({ type }) => - (type.match(VALID_DESIGN_FILE_MIMETYPE.regex) || []).length > 0; - -/** - * Returns formatted array that doesn't contain - * `edges`->`node` nesting - * - * @param {Array} elements - */ - -export const extractNodes = elements => elements.edges.map(({ node }) => node); - -/** - * Returns formatted array of discussions that doesn't contain - * `edges`->`node` nesting for child notes - * - * @param {Array} discussions - */ - -export const extractDiscussions = discussions => - discussions.nodes.map((discussion, index) => ({ - ...discussion, - index: index + 1, - notes: discussion.notes.nodes, - })); - -/** - * Returns a discussion with the given id from discussions array - * - * @param {Array} discussions - */ - -export const extractCurrentDiscussion = (discussions, id) => - discussions.nodes.find(discussion => discussion.id === id); - -export const findVersionId = id => (id.match('::Version/(.+$)') || [])[1]; - -export const findNoteId = id => (id.match('DiffNote/(.+$)') || [])[1]; - -export const extractDesigns = data => data.project.issue.designCollection.designs.edges; - -export const extractDesign = data => (extractDesigns(data) || [])[0]?.node; - -/** - * Generates optimistic response for a design upload mutation - * @param {Array<File>} files - */ -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', - id: -uniqueId(), - image: '', - imageV432x230: '', - filename: file.name, - fullPath: '', - notesCount: 0, - event: 'NONE', - diffRefs: { - __typename: 'DiffRefs', - baseSha: '', - startSha: '', - headSha: '', - }, - discussions: { - __typename: 'DesignDiscussion', - nodes: [], - }, - versions: { - __typename: 'DesignVersionConnection', - edges: { - __typename: 'DesignVersionEdge', - node: { - __typename: 'DesignVersion', - id: -uniqueId(), - sha: -uniqueId(), - }, - }, - }, - })); - - return { - // 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', - designManagementUpload: { - __typename: 'DesignManagementUploadPayload', - designs, - skippedDesigns: [], - errors: [], - }, - }; -}; - -/** - * Generates optimistic response for a design upload mutation - * @param {Array<File>} files - */ -export const updateImageDiffNoteOptimisticResponse = (note, { position }) => ({ - // 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', - updateImageDiffNote: { - __typename: 'UpdateImageDiffNotePayload', - note: { - ...note, - position: { - ...note.position, - ...position, - }, - }, - errors: [], - }, -}); - -const normalizeAuthor = author => ({ - ...author, - web_url: author.webUrl, - avatar_url: author.avatarUrl, -}); - -export const extractParticipants = users => users.edges.map(({ node }) => normalizeAuthor(node)); - -export const getPageLayoutElement = () => document.querySelector('.layout-page'); diff --git a/app/assets/javascripts/design_management_legacy/utils/error_messages.js b/app/assets/javascripts/design_management_legacy/utils/error_messages.js deleted file mode 100644 index 7666c726c2f..00000000000 --- a/app/assets/javascripts/design_management_legacy/utils/error_messages.js +++ /dev/null @@ -1,95 +0,0 @@ -import { __, s__, n__, sprintf } from '~/locale'; - -export const ADD_DISCUSSION_COMMENT_ERROR = s__( - 'DesignManagement|Could not add a new comment. Please try again.', -); - -export const ADD_IMAGE_DIFF_NOTE_ERROR = s__( - 'DesignManagement|Could not create new discussion. Please try again.', -); - -export const UPDATE_IMAGE_DIFF_NOTE_ERROR = s__( - 'DesignManagement|Could not update discussion. Please try again.', -); - -export const UPDATE_NOTE_ERROR = s__('DesignManagement|Could not update note. Please try again.'); - -export const UPLOAD_DESIGN_ERROR = s__( - 'DesignManagement|Error uploading a new design. Please try again.', -); - -export const UPLOAD_DESIGN_INVALID_FILETYPE_ERROR = __( - 'Could not upload your designs as one or more files uploaded are not supported.', -); - -export const DESIGN_NOT_FOUND_ERROR = __('Could not find design.'); - -export const DESIGN_VERSION_NOT_EXIST_ERROR = __('Requested design version does not exist.'); - -const DESIGN_UPLOAD_SKIPPED_MESSAGE = s__('DesignManagement|Upload skipped.'); - -const ALL_DESIGNS_SKIPPED_MESSAGE = `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${s__( - 'The designs you tried uploading did not change.', -)}`; - -export const EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE = __( - 'You can only upload one design when dropping onto an existing design.', -); - -export const EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE = __( - 'You must upload a file with the same file name when dropping onto an existing design.', -); - -const MAX_SKIPPED_FILES_LISTINGS = 5; - -const oneDesignSkippedMessage = filename => - `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${sprintf(s__('DesignManagement|%{filename} did not change.'), { - filename, - })}`; - -/** - * Return warning message indicating that some (but not all) uploaded - * files were skipped. - * @param {Array<{ filename }>} skippedFiles - */ -const someDesignsSkippedMessage = skippedFiles => { - const designsSkippedMessage = `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${s__( - 'Some of the designs you tried uploading did not change:', - )}`; - - const moreText = sprintf(s__(`DesignManagement|and %{moreCount} more.`), { - moreCount: skippedFiles.length - MAX_SKIPPED_FILES_LISTINGS, - }); - - return `${designsSkippedMessage} ${skippedFiles - .slice(0, MAX_SKIPPED_FILES_LISTINGS) - .map(({ filename }) => filename) - .join(', ')}${skippedFiles.length > MAX_SKIPPED_FILES_LISTINGS ? `, ${moreText}` : '.'}`; -}; - -export const designDeletionError = ({ singular = true } = {}) => { - const design = singular ? __('a design') : __('designs'); - return sprintf(s__('Could not delete %{design}. Please try again.'), { - design, - }); -}; - -/** - * Return warning message, if applicable, that one, some or all uploaded - * files were skipped. - * @param {Array<{ filename }>} uploadedDesigns - * @param {Array<{ filename }>} skippedFiles - */ -export const designUploadSkippedWarning = (uploadedDesigns, skippedFiles) => { - if (skippedFiles.length === 0) { - return null; - } - - if (skippedFiles.length === uploadedDesigns.length) { - const { filename } = skippedFiles[0]; - - return n__(oneDesignSkippedMessage(filename), ALL_DESIGNS_SKIPPED_MESSAGE, skippedFiles.length); - } - - return someDesignsSkippedMessage(skippedFiles); -}; diff --git a/app/assets/javascripts/design_management_legacy/utils/tracking.js b/app/assets/javascripts/design_management_legacy/utils/tracking.js deleted file mode 100644 index b3ecc1453a6..00000000000 --- a/app/assets/javascripts/design_management_legacy/utils/tracking.js +++ /dev/null @@ -1,27 +0,0 @@ -import Tracking from '~/tracking'; - -// Tracking Constants -const DESIGN_TRACKING_CONTEXT_SCHEMA = 'iglu:com.gitlab/design_management_context/jsonschema/1-0-0'; -const DESIGN_TRACKING_PAGE_NAME = 'projects:issues:design'; -const DESIGN_TRACKING_EVENT_NAME = 'view_design'; - -// eslint-disable-next-line import/prefer-default-export -export function trackDesignDetailView( - referer = '', - owner = '', - designVersion = 1, - latestVersion = false, -) { - Tracking.event(DESIGN_TRACKING_PAGE_NAME, DESIGN_TRACKING_EVENT_NAME, { - label: DESIGN_TRACKING_EVENT_NAME, - context: { - schema: DESIGN_TRACKING_CONTEXT_SCHEMA, - data: { - 'design-version-number': designVersion, - 'design-is-current-version': latestVersion, - 'internal-object-referrer': referer, - 'design-collection-owner': owner, - }, - }, - }); -} diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js index 0991f5282a8..1de00c9f08b 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js @@ -2,6 +2,7 @@ /* global CommentsStore */ import $ from 'jquery'; +import 'vendor/jquery.scrollTo'; import Vue from 'vue'; import { __ } from '~/locale'; diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 5062006424e..dd5addbf1e3 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -1,6 +1,6 @@ <script> import { mapState, mapGetters, mapActions } from 'vuex'; -import { GlLoadingIcon, GlButtonGroup, GlButton, GlAlert } from '@gitlab/ui'; +import { GlLoadingIcon, GlPagination, GlSprintf } from '@gitlab/ui'; import Mousetrap from 'mousetrap'; import { __ } from '~/locale'; import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils'; @@ -13,9 +13,13 @@ import eventHub from '../../notes/event_hub'; import CompareVersions from './compare_versions.vue'; import DiffFile from './diff_file.vue'; import NoChanges from './no_changes.vue'; -import HiddenFilesWarning from './hidden_files_warning.vue'; import CommitWidget from './commit_widget.vue'; import TreeList from './tree_list.vue'; + +import HiddenFilesWarning from './hidden_files_warning.vue'; +import MergeConflictWarning from './merge_conflict_warning.vue'; +import CollapsedFilesWarning from './collapsed_files_warning.vue'; + import { TREE_LIST_WIDTH_STORAGE_KEY, INITIAL_TREE_WIDTH, @@ -24,6 +28,9 @@ import { TREE_HIDE_STATS_WIDTH, MR_TREE_SHOW_KEY, CENTERED_LIMITED_CONTAINER_CLASSES, + ALERT_OVERFLOW_HIDDEN, + ALERT_MERGE_CONFLICT, + ALERT_COLLAPSED_FILES, } from '../constants'; export default { @@ -33,15 +40,21 @@ export default { DiffFile, NoChanges, HiddenFilesWarning, + MergeConflictWarning, + CollapsedFilesWarning, CommitWidget, TreeList, GlLoadingIcon, PanelResizer, - GlButtonGroup, - GlButton, - GlAlert, + GlPagination, + GlSprintf, }, mixins: [glFeatureFlagsMixin()], + alerts: { + ALERT_OVERFLOW_HIDDEN, + ALERT_MERGE_CONFLICT, + ALERT_COLLAPSED_FILES, + }, props: { endpoint: { type: String, @@ -111,6 +124,7 @@ export default { return { treeWidth, diffFilesLength: 0, + collapsedWarningDismissed: false, }; }, computed: { @@ -139,7 +153,7 @@ export default { 'canMerge', 'hasConflicts', ]), - ...mapGetters('diffs', ['isParallelView', 'currentDiffIndex']), + ...mapGetters('diffs', ['hasCollapsedFile', 'isParallelView', 'currentDiffIndex']), ...mapGetters(['isNotesFetched', 'getNoteableData']), diffs() { if (!this.viewDiffsFileByFile) { @@ -169,6 +183,39 @@ export default { isDiffHead() { return parseBoolean(getParameterByName('diff_head')); }, + showFileByFileNavigation() { + return this.diffFiles.length > 1 && this.viewDiffsFileByFile; + }, + currentFileNumber() { + return this.currentDiffIndex + 1; + }, + previousFileNumber() { + const { currentDiffIndex } = this; + + return currentDiffIndex >= 1 ? currentDiffIndex : null; + }, + nextFileNumber() { + const { currentFileNumber, diffFiles } = this; + + return currentFileNumber < diffFiles.length ? currentFileNumber + 1 : null; + }, + visibleWarning() { + let visible = false; + + if (this.renderOverflowWarning) { + visible = this.$options.alerts.ALERT_OVERFLOW_HIDDEN; + } else if (this.isDiffHead && this.hasConflicts) { + visible = this.$options.alerts.ALERT_MERGE_CONFLICT; + } else if ( + this.hasCollapsedFile && + !this.collapsedWarningDismissed && + !this.viewDiffsFileByFile + ) { + visible = this.$options.alerts.ALERT_COLLAPSED_FILES; + } + + return visible; + }, }, watch: { commit(newCommit, oldCommit) { @@ -186,7 +233,7 @@ export default { } }, diffViewType() { - if (this.needsReload() || this.needsFirstLoad()) { + if (!this.glFeatures.unifiedDiffLines && (this.needsReload() || this.needsFirstLoad())) { this.refetchDiffData(); } this.adjustView(); @@ -212,7 +259,6 @@ export default { projectPath: this.projectPath, dismissEndpoint: this.dismissEndpoint, showSuggestPopover: this.showSuggestPopover, - useSingleDiffStyle: this.glFeatures.singleMrDiffView, viewDiffsFileByFile: this.viewDiffsFileByFile, }); @@ -262,7 +308,6 @@ export default { ...mapActions('diffs', [ 'moveToNeighboringCommit', 'setBaseConfig', - 'fetchDiffFiles', 'fetchDiffFilesMeta', 'fetchDiffFilesBatch', 'fetchCoverageFiles', @@ -274,6 +319,9 @@ export default { 'toggleShowTreeList', 'navigateToDiffFileIndex', ]), + navigateToDiffFileNumber(number) { + this.navigateToDiffFileIndex(number - 1); + }, refetchDiffData() { this.fetchData(false); }, @@ -286,60 +334,35 @@ export default { ); }, needsReload() { - return ( - this.glFeatures.singleMrDiffView && - this.diffFiles.length && - isSingleViewStyle(this.diffFiles[0]) - ); + return this.diffFiles.length && isSingleViewStyle(this.diffFiles[0]); }, needsFirstLoad() { - return this.glFeatures.singleMrDiffView && !this.diffFiles.length; + return !this.diffFiles.length; }, fetchData(toggleTree = true) { - if (this.glFeatures.diffsBatchLoad) { - this.fetchDiffFilesMeta() - .then(({ real_size }) => { - this.diffFilesLength = parseInt(real_size, 10); - if (toggleTree) this.hideTreeListIfJustOneFile(); + this.fetchDiffFilesMeta() + .then(({ real_size }) => { + this.diffFilesLength = parseInt(real_size, 10); + if (toggleTree) this.hideTreeListIfJustOneFile(); - this.startDiffRendering(); - }) - .catch(() => { - createFlash(__('Something went wrong on our end. Please try again!')); - }); - - this.fetchDiffFilesBatch() - .then(() => { - // Guarantee the discussions are assigned after the batch finishes. - // Just watching the length of the discussions or the diff files - // isn't enough, because with split diff loading, neither will - // change when loading the other half of the diff files. - this.setDiscussions(); - }) - .then(() => this.startDiffRendering()) - .catch(() => { - createFlash(__('Something went wrong on our end. Please try again!')); - }); - } else { - this.fetchDiffFiles() - .then(({ real_size }) => { - this.diffFilesLength = parseInt(real_size, 10); - if (toggleTree) { - this.hideTreeListIfJustOneFile(); - } + this.startDiffRendering(); + }) + .catch(() => { + createFlash(__('Something went wrong on our end. Please try again!')); + }); - requestIdleCallback( - () => { - this.setDiscussions(); - this.startRenderDiffsQueue(); - }, - { timeout: 1000 }, - ); - }) - .catch(() => { - createFlash(__('Something went wrong on our end. Please try again!')); - }); - } + this.fetchDiffFilesBatch() + .then(() => { + // Guarantee the discussions are assigned after the batch finishes. + // Just watching the length of the discussions or the diff files + // isn't enough, because with split diff loading, neither will + // change when loading the other half of the diff files. + this.setDiscussions(); + }) + .then(() => this.startDiffRendering()) + .catch(() => { + createFlash(__('Something went wrong on our end. Please try again!')); + }); if (this.endpointCoverage) { this.fetchCoverageFiles(); @@ -406,6 +429,9 @@ export default { this.toggleShowTreeList(false); } }, + dismissCollapsedWarning() { + this.collapsedWarningDismissed = true; + }, }, minTreeWidth: MIN_TREE_WIDTH, maxTreeWidth: MAX_TREE_WIDTH, @@ -423,59 +449,27 @@ export default { /> <hidden-files-warning - v-if="renderOverflowWarning" + v-if="visibleWarning == $options.alerts.ALERT_OVERFLOW_HIDDEN" :visible="numVisibleFiles" :total="numTotalFiles" :plain-diff-path="plainDiffPath" :email-patch-path="emailPatchPath" /> - - <div - v-if="isDiffHead && hasConflicts" - :class="{ - [CENTERED_LIMITED_CONTAINER_CLASSES]: isLimitedContainer, - }" - > - <gl-alert - :dismissible="false" - :title="__('There are merge conflicts')" - variant="warning" - class="w-100 mb-3" - > - <p class="mb-1"> - {{ __('The comparison view may be inaccurate due to merge conflicts.') }} - </p> - <p class="mb-0"> - {{ - __( - 'Resolve these conflicts or ask someone with write access to this repository to merge it locally.', - ) - }} - </p> - <template #actions> - <gl-button - v-if="conflictResolutionPath" - :href="conflictResolutionPath" - variant="info" - class="mr-3 gl-alert-action" - > - {{ __('Resolve conflicts') }} - </gl-button> - <gl-button - v-if="canMerge" - class="gl-alert-action" - data-toggle="modal" - data-target="#modal_merge_info" - > - {{ __('Merge locally') }} - </gl-button> - </template> - </gl-alert> - </div> + <merge-conflict-warning + v-if="visibleWarning == $options.alerts.ALERT_MERGE_CONFLICT" + :limited="isLimitedContainer" + :resolution-path="conflictResolutionPath" + :mergeable="canMerge" + /> + <collapsed-files-warning + v-if="visibleWarning == $options.alerts.ALERT_COLLAPSED_FILES" + :limited="isLimitedContainer" + @dismiss="dismissCollapsedWarning" + /> <div :data-can-create-note="getNoteableData.current_user.can_create_note" - class="files d-flex" + class="files d-flex gl-mt-2" > <div v-if="showTreeList" @@ -509,23 +503,22 @@ export default { :can-current-user-fork="canCurrentUserFork" :view-diffs-file-by-file="viewDiffsFileByFile" /> - <div v-if="viewDiffsFileByFile" class="d-flex gl-justify-content-center"> - <gl-button-group> - <gl-button - :disabled="currentDiffIndex === 0" - data-testid="singleFilePrevious" - @click="navigateToDiffFileIndex(currentDiffIndex - 1)" - > - {{ __('Prev') }} - </gl-button> - <gl-button - :disabled="currentDiffIndex === diffFiles.length - 1" - data-testid="singleFileNext" - @click="navigateToDiffFileIndex(currentDiffIndex + 1)" - > - {{ __('Next') }} - </gl-button> - </gl-button-group> + <div + v-if="showFileByFileNavigation" + data-testid="file-by-file-navigation" + class="gl-display-grid gl-text-center" + > + <gl-pagination + class="gl-mx-auto" + :value="currentFileNumber" + :prev-page="previousFileNumber" + :next-page="nextFileNumber" + @input="navigateToDiffFileNumber" + /> + <gl-sprintf :message="__('File %{current} of %{total}')"> + <template #current>{{ currentFileNumber }}</template> + <template #total>{{ diffFiles.length }}</template> + </gl-sprintf> </div> </template> <no-changes v-else :changes-empty-state-illustration="changesEmptyStateIllustration" /> diff --git a/app/assets/javascripts/diffs/components/collapsed_files_warning.vue b/app/assets/javascripts/diffs/components/collapsed_files_warning.vue new file mode 100644 index 00000000000..dded3643115 --- /dev/null +++ b/app/assets/javascripts/diffs/components/collapsed_files_warning.vue @@ -0,0 +1,71 @@ +<script> +import { mapActions } from 'vuex'; + +import { GlAlert, GlButton } from '@gitlab/ui'; + +import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants'; + +export default { + components: { + GlAlert, + GlButton, + }, + props: { + limited: { + type: Boolean, + required: false, + default: false, + }, + dismissed: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + isDismissed: this.dismissed, + }; + }, + computed: { + containerClasses() { + return { + [CENTERED_LIMITED_CONTAINER_CLASSES]: this.limited, + }; + }, + }, + + methods: { + ...mapActions('diffs', ['expandAllFiles']), + dismiss() { + this.isDismissed = true; + this.$emit('dismiss'); + }, + expand() { + this.expandAllFiles(); + this.dismiss(); + }, + }, +}; +</script> + +<template> + <div v-if="!isDismissed" data-testid="root" :class="containerClasses"> + <gl-alert + :dismissible="true" + :title="__('Some changes are not shown')" + variant="warning" + class="gl-mb-5" + @dismiss="dismiss" + > + <p class="gl-mb-0"> + {{ __('For a faster browsing experience, some files are collapsed by default.') }} + </p> + <template #actions> + <gl-button category="secondary" variant="warning" class="gl-alert-action" @click="expand"> + {{ __('Expand all files') }} + </gl-button> + </template> + </gl-alert> + </div> +</template> diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue index 274a4027e62..23669eecce2 100644 --- a/app/assets/javascripts/diffs/components/commit_item.vue +++ b/app/assets/javascripts/diffs/components/commit_item.vue @@ -1,11 +1,11 @@ <script> +/* eslint-disable vue/no-v-html */ import { mapActions } from 'vuex'; import { GlButtonGroup, GlButton, GlIcon, GlTooltipDirective } 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 Icon from '~/vue_shared/components/icon.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; @@ -39,7 +39,6 @@ import { setUrlParams } from '../../lib/utils/url_utility'; export default { components: { UserAvatarLink, - Icon, ClipboardButton, TimeAgoTooltip, CommitPipelineStatus, @@ -150,14 +149,13 @@ export default { <span class="commit-row-message d-block d-sm-none">· {{ commit.short_id }}</span> - <button + <gl-button v-if="commit.description_html && collapsible" - class="text-expander js-toggle-button" - type="button" + class="js-toggle-button" + size="small" + icon="ellipsis_h" :aria-label="__('Toggle commit description')" - > - <icon :size="12" name="ellipsis_h" /> - </button> + /> <div class="committer"> <a diff --git a/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue index ed4edabd81c..8263e938e69 100644 --- a/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue +++ b/app/assets/javascripts/diffs/components/compare_dropdown_layout.vue @@ -1,10 +1,10 @@ <script> -import Icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; export default { components: { - Icon, + GlIcon, TimeAgo, }, props: { @@ -29,7 +29,7 @@ export default { aria-expanded="false" > <span> {{ selectedVersionName }} </span> - <icon :size="12" name="angle-down" class="position-absolute" /> + <gl-icon :size="12" name="angle-down" class="position-absolute" /> </a> <div class="dropdown-menu dropdown-select dropdown-menu-selectable"> <div class="dropdown-content"> diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue index 35e4527af69..b94874c5644 100644 --- a/app/assets/javascripts/diffs/components/compare_versions.vue +++ b/app/assets/javascripts/diffs/components/compare_versions.vue @@ -1,9 +1,8 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; -import { GlTooltipDirective, GlLink, GlDeprecatedButton, GlSprintf } from '@gitlab/ui'; +import { GlTooltipDirective, GlLink, GlButton, GlSprintf } from '@gitlab/ui'; import { __ } from '~/locale'; import { polyfillSticky } from '~/lib/utils/sticky'; -import Icon from '~/vue_shared/components/icon.vue'; import CompareDropdownLayout from './compare_dropdown_layout.vue'; import SettingsDropdown from './settings_dropdown.vue'; import DiffStats from './diff_stats.vue'; @@ -12,9 +11,8 @@ import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants'; export default { components: { CompareDropdownLayout, - Icon, GlLink, - GlDeprecatedButton, + GlButton, GlSprintf, SettingsDropdown, DiffStats, @@ -84,18 +82,15 @@ export default { [CENTERED_LIMITED_CONTAINER_CLASSES]: isLimitedContainer, }" > - <button + <gl-button v-gl-tooltip.hover - type="button" - class="btn btn-default gl-mr-3 js-toggle-tree-list" - :class="{ - active: showTreeList, - }" + variant="default" + icon="file-tree" + class="gl-mr-3 js-toggle-tree-list" :title="toggleFileBrowserTitle" + :selected="showTreeList" @click="toggleShowTreeList" - > - <icon name="file-tree" /> - </button> + /> <gl-sprintf v-if="showDropdowns" class="d-flex align-items-center compare-versions-container" @@ -124,16 +119,22 @@ export default { :added-lines="addedLines" :removed-lines="removedLines" /> - <gl-deprecated-button + <gl-button v-if="commit || startVersion" :href="latestVersionPath" + variant="default" class="gl-mr-3 js-latest-version" > {{ __('Show latest version') }} - </gl-deprecated-button> - <gl-deprecated-button v-show="hasCollapsedFile" class="gl-mr-3" @click="expandAllFiles"> + </gl-button> + <gl-button + v-show="hasCollapsedFile" + variant="default" + class="gl-mr-3" + @click="expandAllFiles" + > {{ __('Expand all') }} - </gl-deprecated-button> + </gl-button> <settings-dropdown /> </div> </div> diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue index 087a558efdc..9ecb9a44443 100644 --- a/app/assets/javascripts/diffs/components/diff_content.vue +++ b/app/assets/javascripts/diffs/components/diff_content.vue @@ -1,6 +1,7 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; import { GlLoadingIcon } from '@gitlab/ui'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form'; import draftCommentsMixin from '~/diffs/mixins/draft_comments'; import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; @@ -32,7 +33,7 @@ export default { userAvatarLink, DiffFileDrafts, }, - mixins: [diffLineNoteFormMixin, draftCommentsMixin], + mixins: [diffLineNoteFormMixin, draftCommentsMixin, glFeatureFlagsMixin()], props: { diffFile: { type: Object, @@ -48,8 +49,12 @@ export default { ...mapState({ projectPath: state => state.diffs.projectPath, }), - ...mapGetters('diffs', ['isInlineView', 'isParallelView']), - ...mapGetters('diffs', ['getCommentFormForDiffFile']), + ...mapGetters('diffs', [ + 'isInlineView', + 'isParallelView', + 'getCommentFormForDiffFile', + 'diffLines', + ]), ...mapGetters(['getNoteableData', 'noteableType', 'getUserData']), diffMode() { return getDiffMode(this.diffFile); @@ -114,13 +119,15 @@ export default { <inline-diff-view v-if="isInlineView" :diff-file="diffFile" - :diff-lines="diffFile.highlighted_diff_lines || []" + :diff-lines="diffFile.highlighted_diff_lines" :help-page-path="helpPagePath" /> <parallel-diff-view v-else-if="isParallelView" :diff-file="diffFile" - :diff-lines="diffFile.parallel_diff_lines || []" + :diff-lines=" + glFeatures.unifiedDiffLines ? diffLines(diffFile) : diffFile.parallel_diff_lines || [] + " :help-page-path="helpPagePath" /> <gl-loading-icon v-if="diffFile.renderingLines" size="md" class="mt-3" /> diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue index b6a0724c201..7b55bd2104d 100644 --- a/app/assets/javascripts/diffs/components/diff_discussions.vue +++ b/app/assets/javascripts/diffs/components/diff_discussions.vue @@ -1,12 +1,12 @@ <script> import { mapActions } from 'vuex'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import noteableDiscussion from '../../notes/components/noteable_discussion.vue'; export default { components: { noteableDiscussion, - Icon, + GlIcon, }, props: { discussions: { @@ -70,7 +70,7 @@ export default { class="js-diff-notes-toggle" @click="toggleDiscussion({ discussionId: discussion.id })" > - <icon v-if="discussion.expanded" name="collapse" class="collapse-icon" /> + <gl-icon v-if="discussion.expanded" name="collapse" class="collapse-icon" /> <template v-else> {{ index + 1 }} </template> diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue index e5e63bdcb43..0094b4f8707 100644 --- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue +++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue @@ -1,8 +1,9 @@ <script> import { mapState, mapActions } from 'vuex'; +import { GlIcon } from '@gitlab/ui'; import { deprecatedCreateFlash as createFlash } from '~/flash'; -import { s__ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; +import { s__, sprintf } from '~/locale'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { UNFOLD_COUNT, INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '../constants'; import * as utils from '../store/utils'; import tooltip from '../../vue_shared/directives/tooltip'; @@ -17,17 +18,23 @@ const lineNumberByViewType = (viewType, diffLine) => { [PARALLEL_DIFF_VIEW_TYPE]: line => (line?.right || line?.left)?.new_line, }; const numberGetter = numberGetters[viewType]; - return numberGetter && numberGetter(diffLine); }; +const i18n = { + showMore: sprintf(s__('Diffs|Show %{unfoldCount} lines'), { unfoldCount: UNFOLD_COUNT }), + showAll: s__('Diffs|Show all unchanged lines'), +}; + export default { + i18n, directives: { tooltip, }, components: { - Icon, + GlIcon, }, + mixins: [glFeatureFlagsMixin()], props: { fileHash: { type: String, @@ -59,7 +66,9 @@ export default { }, computed: { ...mapState({ - diffViewType: state => state.diffs.diffViewType, + diffViewType(state) { + return this.glFeatures.unifiedDiffLines ? INLINE_DIFF_VIEW_TYPE : state.diffs.diffViewType; + }, diffFiles: state => state.diffs.diffFiles, }), canExpandUp() { @@ -226,32 +235,27 @@ export default { </script> <template> - <td :colspan="colspan" class="text-center"> + <td :colspan="colspan" class="text-center gl-font-regular"> <div class="content js-line-expansion-content"> <a - v-if="canExpandUp" - v-tooltip - class="cursor-pointer js-unfold unfold-icon d-inline-block pt-2 pb-2" - data-placement="top" - data-container="body" - :title="__('Expand up')" - @click="handleExpandLines(EXPAND_UP)" + v-if="canExpandDown" + class="gl-mx-2 gl-cursor-pointer js-unfold-down gl-display-inline-block gl-py-4" + @click="handleExpandLines(EXPAND_DOWN)" > - <icon :size="12" name="expand-up" aria-hidden="true" /> + <gl-icon :size="12" name="expand-down" aria-hidden="true" /> + <span>{{ $options.i18n.showMore }}</span> </a> - <a class="mx-2 cursor-pointer js-unfold-all" @click="handleExpandLines()"> - <span>{{ s__('Diffs|Show unchanged lines') }}</span> + <a class="gl-mx-2 cursor-pointer js-unfold-all" @click="handleExpandLines()"> + <gl-icon :size="12" name="expand" aria-hidden="true" /> + <span>{{ $options.i18n.showAll }}</span> </a> <a - v-if="canExpandDown" - v-tooltip - class="cursor-pointer js-unfold-down has-tooltip unfold-icon d-inline-block pt-2 pb-2" - data-placement="top" - data-container="body" - :title="__('Expand down')" - @click="handleExpandLines(EXPAND_DOWN)" + v-if="canExpandUp" + class="gl-mx-2 gl-cursor-pointer js-unfold gl-display-inline-block gl-py-4" + @click="handleExpandLines(EXPAND_UP)" > - <icon :size="12" name="expand-down" aria-hidden="true" /> + <gl-icon :size="12" name="expand-up" aria-hidden="true" /> + <span>{{ $options.i18n.showMore }}</span> </a> </div> </td> diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index eace673c2d7..9a7ed76bad3 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -1,23 +1,32 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; import { escape } from 'lodash'; -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlButton, GlLoadingIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { __, sprintf } from '~/locale'; +import { sprintf } from '~/locale'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { hasDiff } from '~/helpers/diffs_helper'; import eventHub from '../../notes/event_hub'; import DiffFileHeader from './diff_file_header.vue'; import DiffContent from './diff_content.vue'; import { diffViewerErrors } from '~/ide/constants'; +import { GENERIC_ERROR, DIFF_FILE } from '../i18n'; export default { components: { DiffFileHeader, DiffContent, + GlButton, GlLoadingIcon, }, + directives: { + SafeHtml, + }, mixins: [glFeatureFlagsMixin()], + i18n: { + genericError: GENERIC_ERROR, + ...DIFF_FILE, + }, props: { file: { type: Object, @@ -50,7 +59,7 @@ export default { ...mapGetters('diffs', ['getDiffFileDiscussions']), viewBlobLink() { return sprintf( - __('You can %{linkStart}view the blob%{linkEnd} instead.'), + this.$options.i18n.blobView, { linkStart: `<a href="${escape(this.file.view_path)}">`, linkEnd: '</a>', @@ -72,9 +81,7 @@ export default { }, forkMessage() { return sprintf( - __( - "You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.", - ), + this.$options.i18n.editInFork, { tag_start: '<span class="js-file-fork-suggestion-section-action">', tag_end: '</span>', @@ -93,11 +100,7 @@ export default { }, 'file.file_hash': { handler: function watchFileHash() { - if ( - this.glFeatures.autoExpandCollapsedDiffs && - this.viewDiffsFileByFile && - this.file.viewer.collapsed - ) { + if (this.viewDiffsFileByFile && this.file.viewer.collapsed) { this.isCollapsed = false; this.handleLoadCollapsedDiff(); } else { @@ -107,7 +110,7 @@ export default { immediate: true, }, 'file.viewer.collapsed': function setIsCollapsed(newVal) { - if (!this.viewDiffsFileByFile && !this.glFeatures.autoExpandCollapsedDiffs) { + if (!this.viewDiffsFileByFile) { this.isCollapsed = newVal; } }, @@ -149,7 +152,7 @@ export default { }) .catch(() => { this.isLoadingCollapsedDiff = false; - createFlash(__('Something went wrong on our end. Please try again!')); + createFlash(this.$options.i18n.genericError); }); }, showForkMessage() { @@ -185,32 +188,38 @@ export default { /> <div v-if="forkMessageVisible" class="js-file-fork-suggestion-section file-fork-suggestion"> - <span class="file-fork-suggestion-note" v-html="forkMessage"></span> + <span v-safe-html="forkMessage" class="file-fork-suggestion-note"></span> <a :href="file.fork_path" class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success" - >{{ __('Fork') }}</a + >{{ $options.i18n.fork }}</a > <button class="js-cancel-fork-suggestion-button btn btn-grouped" type="button" @click="hideForkMessage" > - {{ __('Cancel') }} + {{ $options.i18n.cancel }} </button> </div> <gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" /> <template v-else> <div :id="`diff-content-${file.file_hash}`"> <div v-if="errorMessage" class="diff-viewer"> - <div class="nothing-here-block" v-html="errorMessage"></div> + <div v-safe-html="errorMessage" class="nothing-here-block"></div> </div> <template v-else> - <div v-show="isCollapsed" class="nothing-here-block diff-collapsed"> - {{ __('This diff is collapsed.') }} - <a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{ - __('Click to expand it.') - }}</a> + <div v-show="isCollapsed" class="gl-p-7 gl-text-center collapsed-file-warning"> + <p class="gl-mb-8 gl-mt-5"> + {{ $options.i18n.collapsed }} + </p> + <gl-button + class="gl-alert-action gl-mb-5" + data-testid="expandButton" + @click="handleToggle" + > + {{ $options.i18n.expand }} + </gl-button> </div> <diff-content v-show="!isCollapsed && !isFileTooLarge" diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 5727fbaaf68..fded391cc84 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -1,9 +1,16 @@ <script> +/* eslint-disable vue/no-v-html */ import { escape } from 'lodash'; import { mapActions, mapGetters } from 'vuex'; -import { GlDeprecatedButton, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui'; +import { + GlDeprecatedButton, + GlTooltipDirective, + GlSafeHtmlDirective, + GlLoadingIcon, + GlIcon, + GlButton, +} from '@gitlab/ui'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import Icon from '~/vue_shared/components/icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import { truncateSha } from '~/lib/utils/text_utility'; import { __, s__, sprintf } from '~/locale'; @@ -18,12 +25,14 @@ export default { GlDeprecatedButton, ClipboardButton, EditButton, - Icon, + GlIcon, FileIcon, DiffStats, + GlButton, }, directives: { GlTooltip: GlTooltipDirective, + SafeHtml: GlSafeHtmlDirective, }, props: { discussionPath: { @@ -77,6 +86,21 @@ export default { return this.discussionPath; }, + submoduleDiffCompareLinkText() { + if (this.diffFile.submodule_compare) { + const truncatedOldSha = escape(truncateSha(this.diffFile.submodule_compare.old_sha)); + const truncatedNewSha = escape(truncateSha(this.diffFile.submodule_compare.new_sha)); + return sprintf( + s__('Compare %{oldCommitId}...%{newCommitId}'), + { + oldCommitId: `<span class="commit-sha">${truncatedOldSha}</span>`, + newCommitId: `<span class="commit-sha">${truncatedNewSha}</span>`, + }, + false, + ); + } + return null; + }, filePath() { if (this.diffFile.submodule) { return `${this.diffFile.file_path} @ ${truncateSha(this.diffFile.blob.id)}`; @@ -133,6 +157,7 @@ export default { 'toggleFileDiscussions', 'toggleFileDiscussionWrappers', 'toggleFullDiff', + 'toggleActiveFileByHash', ]), handleToggleFile() { this.$emit('toggleFile'); @@ -149,6 +174,9 @@ export default { const selector = this.diffContentIDSelector; scrollToElement(document.querySelector(selector)); window.location.hash = selector; + if (!this.viewDiffsFileByFile) { + this.toggleActiveFileByHash(this.diffFile.file_hash); + } } }, }, @@ -162,7 +190,7 @@ export default { @click.self="handleToggleFile" > <div class="file-header-content"> - <icon + <gl-icon v-if="collapsible" ref="collapseIcon" :name="collapseIcon" @@ -237,7 +265,7 @@ export default { type="button" @click="toggleFileDiscussionWrappers(diffFile)" > - <icon name="comment" /> + <gl-icon name="comment" /> </gl-deprecated-button> </span> @@ -273,8 +301,8 @@ export default { @click="toggleFullDiff(diffFile.file_path)" > <gl-loading-icon v-if="diffFile.isLoadingFullFile" color="dark" inline /> - <icon v-else-if="diffFile.isShowingFullFile" name="doc-changes" /> - <icon v-else name="doc-expand" /> + <gl-icon v-else-if="diffFile.isShowingFullFile" name="doc-changes" /> + <gl-icon v-else name="doc-expand" /> </gl-deprecated-button> <gl-deprecated-button ref="viewButton" @@ -287,7 +315,7 @@ export default { data-track-property="diff_toggle_view_sha" :title="viewFileButtonText" > - <icon name="doc-text" /> + <gl-icon name="doc-text" /> </gl-deprecated-button> <a @@ -303,9 +331,22 @@ export default { data-track-property="diff_toggle_external" class="btn btn-file-option" > - <icon name="external-link" /> + <gl-icon name="external-link" /> </a> </div> </div> + + <div + v-if="diffFile.submodule_compare" + class="file-actions d-none d-sm-flex align-items-center flex-wrap" + > + <gl-button + v-gl-tooltip.hover + v-safe-html="submoduleDiffCompareLinkText" + class="submodule-compare" + :title="s__('Compare submodule commit revisions')" + :href="diffFile.submodule_compare.url" + /> + </div> </div> </template> diff --git a/app/assets/javascripts/diffs/components/diff_file_row.vue b/app/assets/javascripts/diffs/components/diff_file_row.vue index 43b669625f4..2856e6ae8eb 100644 --- a/app/assets/javascripts/diffs/components/diff_file_row.vue +++ b/app/assets/javascripts/diffs/components/diff_file_row.vue @@ -3,9 +3,10 @@ * This component is an iterative step towards refactoring and simplifying `vue_shared/components/file_row.vue` * https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23720 */ +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import FileRow from '~/vue_shared/components/file_row.vue'; -import FileRowStats from './file_row_stats.vue'; import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; +import FileRowStats from './file_row_stats.vue'; export default { name: 'DiffFileRow', @@ -14,6 +15,7 @@ export default { FileRowStats, ChangedFileIcon, }, + mixins: [glFeatureFlagsMixin()], props: { file: { type: Object, @@ -28,11 +30,28 @@ export default { required: false, default: null, }, + viewedFiles: { + type: Object, + required: false, + default: () => ({}), + }, }, computed: { showFileRowStats() { return !this.hideFileStats && this.file.type === 'blob'; }, + fileClasses() { + if (!this.glFeatures.highlightCurrentDiffRow) { + return ''; + } + + return this.file.type === 'blob' && !this.viewedFiles[this.file.fileHash] + ? 'gl-font-weight-bold' + : ''; + }, + isActive() { + return this.currentDiffFileId === this.file.fileHash; + }, }, }; </script> @@ -41,8 +60,9 @@ export default { <file-row :file="file" v-bind="$attrs" - :class="{ 'is-active': currentDiffFileId === file.fileHash }" + :class="{ 'is-active': isActive }" class="diff-file-row" + :file-classes="fileClasses" v-on="$listeners" > <file-row-stats v-if="showFileRowStats" :file="file" class="mr-1" /> diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue index be19d8520b5..439319f487c 100644 --- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue +++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue @@ -1,14 +1,13 @@ <script> -import { GlTooltipDirective } from '@gitlab/ui'; +import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { n__ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; import { truncate } from '~/lib/utils/text_utility'; import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants'; export default { components: { - Icon, + GlIcon, UserAvatarImage, }, directives: { @@ -68,7 +67,7 @@ export default { class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button" @click="$emit('toggleLineDiscussions')" > - <icon :size="12" name="collapse" /> + <gl-icon :size="12" name="collapse" /> </button> <template v-else> <user-avatar-image diff --git a/app/assets/javascripts/diffs/components/diff_stats.vue b/app/assets/javascripts/diffs/components/diff_stats.vue index 439d8097e56..05fbbd39fae 100644 --- a/app/assets/javascripts/diffs/components/diff_stats.vue +++ b/app/assets/javascripts/diffs/components/diff_stats.vue @@ -1,10 +1,10 @@ <script> import { isNumber } from 'lodash'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import { n__ } from '~/locale'; export default { - components: { Icon }, + components: { GlIcon }, props: { addedLines: { type: Number, @@ -46,7 +46,7 @@ export default { }" > <div v-if="hasDiffFiles" class="diff-stats-group"> - <icon name="doc-code" class="diff-stats-icon text-secondary" /> + <gl-icon name="doc-code" class="diff-stats-icon text-secondary" /> <span class="text-secondary bold">{{ diffFilesCountText }} {{ filesText }}</span> </div> <div diff --git a/app/assets/javascripts/diffs/components/edit_button.vue b/app/assets/javascripts/diffs/components/edit_button.vue index 21fdb19287d..ff1af5569dc 100644 --- a/app/assets/javascripts/diffs/components/edit_button.vue +++ b/app/assets/javascripts/diffs/components/edit_button.vue @@ -1,12 +1,11 @@ <script> -import { GlTooltipDirective, GlDeprecatedButton } from '@gitlab/ui'; +import { GlTooltipDirective, GlDeprecatedButton, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; export default { components: { GlDeprecatedButton, - Icon, + GlIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -59,7 +58,7 @@ export default { class="rounded-0 js-edit-blob" @click.native="handleEditClick" > - <icon name="pencil" /> + <gl-icon name="pencil" /> </gl-deprecated-button> </span> </template> diff --git a/app/assets/javascripts/diffs/components/image_diff_overlay.vue b/app/assets/javascripts/diffs/components/image_diff_overlay.vue index be7e6789216..3956c2fab49 100644 --- a/app/assets/javascripts/diffs/components/image_diff_overlay.vue +++ b/app/assets/javascripts/diffs/components/image_diff_overlay.vue @@ -2,12 +2,12 @@ import { mapActions, mapGetters } from 'vuex'; import { isArray } from 'lodash'; import imageDiffMixin from 'ee_else_ce/diffs/mixins/image_diff'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; export default { name: 'ImageDiffOverlay', components: { - Icon, + GlIcon, }, mixins: [imageDiffMixin], props: { @@ -112,7 +112,7 @@ export default { type="button" @click="clickedToggle(discussion)" > - <icon v-if="showCommentIcon" name="image-comment-dark" /> + <gl-icon v-if="showCommentIcon" name="image-comment-dark" /> <template v-else> {{ toggleText(discussion, index) }} </template> @@ -127,7 +127,7 @@ export default { class="btn-transparent comment-indicator" type="button" > - <icon name="image-comment-dark" /> + <gl-icon name="image-comment-dark" /> </button> </div> </template> 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 168e8c6c14e..7fab750089e 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue @@ -1,7 +1,6 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; -import { GlTooltipDirective } from '@gitlab/ui'; -import DiffTableCell from './diff_table_cell.vue'; +import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import { MATCH_LINE_TYPE, NEW_LINE_TYPE, @@ -10,14 +9,23 @@ import { CONTEXT_LINE_CLASS_NAME, LINE_POSITION_LEFT, LINE_POSITION_RIGHT, + LINE_HOVER_CLASS_NAME, + OLD_NO_NEW_LINE_TYPE, + NEW_NO_NEW_LINE_TYPE, + EMPTY_CELL_TYPE, } from '../constants'; +import { __ } from '~/locale'; +import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils'; +import DiffGutterAvatars from './diff_gutter_avatars.vue'; export default { components: { - DiffTableCell, + DiffGutterAvatars, + GlIcon, }, directives: { GlTooltip: GlTooltipDirective, + SafeHtml, }, props: { fileHash: { @@ -49,6 +57,7 @@ export default { }; }, computed: { + ...mapGetters(['isLoggedIn']), ...mapGetters('diffs', ['fileLineCoverage']), ...mapState({ isHighlighted(state) { @@ -78,6 +87,70 @@ export default { coverageState() { return this.fileLineCoverage(this.filePath, this.line.new_line); }, + isMetaLine() { + const { type } = this.line; + + return ( + type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE + ); + }, + classNameMapCell() { + const { type } = this.line; + + return [ + type, + { + hll: this.isHighlighted, + [LINE_HOVER_CLASS_NAME]: + this.isLoggedIn && this.isHover && !this.isContextLine && !this.isMetaLine, + }, + ]; + }, + addCommentTooltip() { + const brokenSymlinks = this.line.commentsDisabled; + let tooltip = __('Add a comment to this line'); + + if (brokenSymlinks) { + if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) { + tooltip = __( + 'Commenting on symbolic links that replace or are replaced by files is currently not supported.', + ); + } else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) { + tooltip = __( + 'Commenting on files that replace or are replaced by symbolic links is currently not supported.', + ); + } + } + + return tooltip; + }, + shouldRenderCommentButton() { + if (this.isLoggedIn) { + const isDiffHead = parseBoolean(getParameterByName('diff_head')); + return !isDiffHead || gon.features?.mergeRefHeadComments; + } + + return false; + }, + shouldShowCommentButton() { + return this.isHover && !this.isContextLine && !this.isMetaLine && !this.hasDiscussions; + }, + hasDiscussions() { + return this.line.discussions && this.line.discussions.length > 0; + }, + lineHref() { + return `#${this.line.line_code || ''}`; + }, + lineCode() { + return ( + this.line.line_code || + (this.line.left && this.line.left.line_code) || + (this.line.right && this.line.right.line_code) + ); + }, + shouldShowAvatarsOnGutter() { + return this.hasDiscussions; + }, }, created() { this.newLineType = NEW_LINE_TYPE; @@ -89,12 +162,20 @@ export default { this.scrollToLineIfNeededInline(this.line); }, methods: { - ...mapActions('diffs', ['scrollToLineIfNeededInline']), + ...mapActions('diffs', [ + 'scrollToLineIfNeededInline', + 'showCommentForm', + 'setHighlightedRow', + 'toggleLineDiscussions', + ]), handleMouseMove(e) { // To show the comment icon on the gutter we need to know if we hover the line. // Current table structure doesn't allow us to do this with CSS in both of the diff view types this.isHover = e.type === 'mouseover'; }, + handleCommentButton() { + this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash }); + }, }, }; </script> @@ -108,25 +189,52 @@ export default { @mouseover="handleMouseMove" @mouseout="handleMouseMove" > - <diff-table-cell - :file-hash="fileHash" - :line="line" - :line-type="oldLineType" - :is-bottom="isBottom" - :is-hover="isHover" - :show-comment-button="true" - :is-highlighted="isHighlighted" - class="diff-line-num old_line" - /> - <diff-table-cell - :file-hash="fileHash" - :line="line" - :line-type="newLineType" - :is-bottom="isBottom" - :is-hover="isHover" - :is-highlighted="isHighlighted" - class="diff-line-num new_line qa-new-diff-line" - /> + <td ref="oldTd" class="diff-line-num old_line" :class="classNameMapCell"> + <span + v-if="shouldRenderCommentButton" + ref="addNoteTooltip" + v-gl-tooltip + class="add-diff-note tooltip-wrapper" + :title="addCommentTooltip" + > + <button + v-show="shouldShowCommentButton" + ref="addDiffNoteButton" + type="button" + class="add-diff-note note-button js-add-diff-note-button qa-diff-comment" + :disabled="line.commentsDisabled" + @click="handleCommentButton" + > + <gl-icon :size="12" name="comment" /> + </button> + </span> + <a + v-if="line.old_line" + ref="lineNumberRefOld" + :data-linenumber="line.old_line" + :href="lineHref" + @click="setHighlightedRow(lineCode)" + > + </a> + <diff-gutter-avatars + v-if="shouldShowAvatarsOnGutter" + :discussions="line.discussions" + :discussions-expanded="line.discussionsExpanded" + @toggleLineDiscussions=" + toggleLineDiscussions({ lineCode, fileHash, expanded: !line.discussionsExpanded }) + " + /> + </td> + <td ref="newTd" class="diff-line-num new_line qa-new-diff-line" :class="classNameMapCell"> + <a + v-if="line.new_line" + ref="lineNumberRefNew" + :data-linenumber="line.new_line" + :href="lineHref" + @click="setHighlightedRow(lineCode)" + > + </a> + </td> <td v-gl-tooltip.hover :title="coverageState.text" @@ -134,6 +242,7 @@ export default { class="line-coverage" ></td> <td + v-safe-html="line.rich_text" :class="[ line.type, { @@ -141,7 +250,6 @@ export default { }, ]" class="line_content with-coverage" - v-html="line.rich_text" ></td> </tr> </template> diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue index e82d06ee385..13805910648 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_view.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue @@ -1,5 +1,6 @@ <script> import { mapGetters, mapState } from 'vuex'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import draftCommentsMixin from '~/diffs/mixins/draft_comments'; import InlineDraftCommentRow from '~/batch_comments/components/inline_draft_comment_row.vue'; import inlineDiffTableRow from './inline_diff_table_row.vue'; @@ -14,7 +15,7 @@ export default { InlineDraftCommentRow, inlineDiffExpansionRow, }, - mixins: [draftCommentsMixin], + mixins: [draftCommentsMixin, glFeatureFlagsMixin()], props: { diffFile: { type: Object, diff --git a/app/assets/javascripts/diffs/components/merge_conflict_warning.vue b/app/assets/javascripts/diffs/components/merge_conflict_warning.vue new file mode 100644 index 00000000000..e47bea8e589 --- /dev/null +++ b/app/assets/javascripts/diffs/components/merge_conflict_warning.vue @@ -0,0 +1,72 @@ +<script> +import { GlButton, GlAlert } from '@gitlab/ui'; +import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants'; + +export default { + components: { + GlAlert, + GlButton, + }, + props: { + limited: { + type: Boolean, + required: true, + }, + mergeable: { + type: Boolean, + required: true, + }, + resolutionPath: { + type: String, + required: true, + }, + }, + computed: { + containerClasses() { + return { + [CENTERED_LIMITED_CONTAINER_CLASSES]: this.limited, + }; + }, + }, +}; +</script> + +<template> + <div :class="containerClasses"> + <gl-alert + :dismissible="false" + :title="__('There are merge conflicts')" + variant="warning" + class="gl-mb-5" + > + <p class="gl-mb-2"> + {{ __('The comparison view may be inaccurate due to merge conflicts.') }} + </p> + <p class="gl-mb-0"> + {{ + __( + 'Resolve these conflicts or ask someone with write access to this repository to merge it locally.', + ) + }} + </p> + <template #actions> + <gl-button + v-if="resolutionPath" + :href="resolutionPath" + variant="info" + class="gl-mr-5 gl-alert-action" + > + {{ __('Resolve conflicts') }} + </gl-button> + <gl-button + v-if="mergeable" + class="gl-alert-action" + data-toggle="modal" + data-target="#modal_merge_info" + > + {{ __('Merge locally') }} + </gl-button> + </template> + </gl-alert> + </div> +</template> diff --git a/app/assets/javascripts/diffs/components/no_changes.vue b/app/assets/javascripts/diffs/components/no_changes.vue index 93afa978862..a640dcb0a90 100644 --- a/app/assets/javascripts/diffs/components/no_changes.vue +++ b/app/assets/javascripts/diffs/components/no_changes.vue @@ -1,12 +1,11 @@ <script> import { mapGetters } from 'vuex'; -import { escape } from 'lodash'; -import { GlButton } from '@gitlab/ui'; -import { __, sprintf } from '~/locale'; +import { GlButton, GlSprintf } from '@gitlab/ui'; export default { components: { GlButton, + GlSprintf, }, props: { changesEmptyStateIllustration: { @@ -16,20 +15,6 @@ export default { }, computed: { ...mapGetters(['getNoteableData']), - emptyStateText() { - return sprintf( - __( - 'No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}', - ), - { - ref_start: '<span class="ref-name">', - ref_end: '</span>', - source_branch: escape(this.getNoteableData.source_branch), - target_branch: escape(this.getNoteableData.target_branch), - }, - false, - ); - }, }, }; </script> @@ -41,7 +26,14 @@ export default { </div> <div class="col-12"> <div class="text-content text-center"> - <span v-html="emptyStateText"></span> + <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 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 ccb32a2a745..0bf47dc77a6 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue @@ -1,8 +1,7 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; import $ from 'jquery'; -import { GlTooltipDirective } from '@gitlab/ui'; -import DiffTableCell from './diff_table_cell.vue'; +import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import { MATCH_LINE_TYPE, NEW_LINE_TYPE, @@ -13,14 +12,20 @@ import { PARALLEL_DIFF_VIEW_TYPE, NEW_NO_NEW_LINE_TYPE, EMPTY_CELL_TYPE, + LINE_HOVER_CLASS_NAME, } from '../constants'; +import { __ } from '~/locale'; +import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils'; +import DiffGutterAvatars from './diff_gutter_avatars.vue'; export default { components: { - DiffTableCell, + GlIcon, + DiffGutterAvatars, }, directives: { GlTooltip: GlTooltipDirective, + SafeHtml, }, props: { fileHash: { @@ -50,10 +55,12 @@ export default { return { isLeftHover: false, isRightHover: false, + isCommentButtonRendered: false, }; }, computed: { ...mapGetters('diffs', ['fileLineCoverage']), + ...mapGetters(['isLoggedIn']), ...mapState({ isHighlighted(state) { if (this.isCommented) return true; @@ -65,12 +72,15 @@ export default { return lineCode ? lineCode === state.diffs.highlightedRow : false; }, }), - isContextLine() { + isContextLineLeft() { return this.line.left && this.line.left.type === CONTEXT_LINE_TYPE; }, + isContextLineRight() { + return this.line.right && this.line.right.type === CONTEXT_LINE_TYPE; + }, classNameMap() { return { - [CONTEXT_LINE_CLASS_NAME]: this.isContextLine, + [CONTEXT_LINE_CLASS_NAME]: this.isContextLineLeft, [PARALLEL_DIFF_VIEW_TYPE]: true, }; }, @@ -97,6 +107,129 @@ export default { coverageState() { return this.fileLineCoverage(this.filePath, this.line.right.new_line); }, + classNameMapCellLeft() { + const { type } = this.line.left; + + return [ + type, + { + hll: this.isHighlighted, + [LINE_HOVER_CLASS_NAME]: + this.isLoggedIn && this.isLeftHover && !this.isContextLineLeft && !this.isMetaLineLeft, + }, + ]; + }, + classNameMapCellRight() { + const { type } = this.line.right; + + return [ + type, + { + hll: this.isHighlighted, + [LINE_HOVER_CLASS_NAME]: + this.isLoggedIn && + this.isRightHover && + !this.isContextLineRight && + !this.isMetaLineRight, + }, + ]; + }, + addCommentTooltipLeft() { + const brokenSymlinks = this.line.left.commentsDisabled; + let tooltip = __('Add a comment to this line'); + + if (brokenSymlinks) { + if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) { + tooltip = __( + 'Commenting on symbolic links that replace or are replaced by files is currently not supported.', + ); + } else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) { + tooltip = __( + 'Commenting on files that replace or are replaced by symbolic links is currently not supported.', + ); + } + } + + return tooltip; + }, + addCommentTooltipRight() { + const brokenSymlinks = this.line.right.commentsDisabled; + let tooltip = __('Add a comment to this line'); + + if (brokenSymlinks) { + if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) { + tooltip = __( + 'Commenting on symbolic links that replace or are replaced by files is currently not supported.', + ); + } else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) { + tooltip = __( + 'Commenting on files that replace or are replaced by symbolic links is currently not supported.', + ); + } + } + + return tooltip; + }, + shouldRenderCommentButton() { + if (!this.isCommentButtonRendered) { + return false; + } + + if (this.isLoggedIn) { + const isDiffHead = parseBoolean(getParameterByName('diff_head')); + return !isDiffHead || gon.features?.mergeRefHeadComments; + } + + return false; + }, + shouldShowCommentButtonLeft() { + return ( + this.isLeftHover && + !this.isContextLineLeft && + !this.isMetaLineLeft && + !this.hasDiscussionsLeft + ); + }, + shouldShowCommentButtonRight() { + return ( + this.isRightHover && + !this.isContextLineRight && + !this.isMetaLineRight && + !this.hasDiscussionsRight + ); + }, + hasDiscussionsLeft() { + return this.line.left?.discussions?.length > 0; + }, + hasDiscussionsRight() { + return this.line.right?.discussions?.length > 0; + }, + lineHrefOld() { + return `#${this.line.left.line_code || ''}`; + }, + lineHrefNew() { + return `#${this.line.right.line_code || ''}`; + }, + lineCode() { + return ( + (this.line.left && this.line.left.line_code) || + (this.line.right && this.line.right.line_code) + ); + }, + isMetaLineLeft() { + const type = this.line.left?.type; + + return ( + type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE + ); + }, + isMetaLineRight() { + const type = this.line.right?.type; + + return ( + type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE + ); + }, }, created() { this.newLineType = NEW_LINE_TYPE; @@ -105,9 +238,26 @@ export default { }, mounted() { this.scrollToLineIfNeededParallel(this.line); + this.unwatchShouldShowCommentButton = this.$watch( + vm => [vm.shouldShowCommentButtonLeft, vm.shouldShowCommentButtonRight].join(), + newVal => { + if (newVal) { + this.isCommentButtonRendered = true; + this.unwatchShouldShowCommentButton(); + } + }, + ); + }, + beforeDestroy() { + this.unwatchShouldShowCommentButton(); }, methods: { - ...mapActions('diffs', ['scrollToLineIfNeededParallel']), + ...mapActions('diffs', [ + 'scrollToLineIfNeededParallel', + 'showCommentForm', + 'setHighlightedRow', + 'toggleLineDiscussions', + ]), handleMouseMove(e) { const isHover = e.type === 'mouseover'; const hoveringCell = e.target.closest('td'); @@ -133,6 +283,9 @@ export default { table.addClass(`${lineClass}-selected`); } }, + handleCommentButton(line) { + this.showCommentForm({ lineCode: line.line_code, fileHash: this.fileHash }); + }, }, }; </script> @@ -145,25 +298,53 @@ export default { @mouseout="handleMouseMove" > <template v-if="line.left && !isMatchLineLeft"> - <diff-table-cell - :file-hash="fileHash" - :line="line.left" - :line-type="oldLineType" - :is-bottom="isBottom" - :is-hover="isLeftHover" - :is-highlighted="isHighlighted" - :show-comment-button="true" - :diff-view-type="parallelDiffViewType" - line-position="left" - class="diff-line-num old_line" - /> + <td ref="oldTd" :class="classNameMapCellLeft" class="diff-line-num old_line"> + <span + v-if="shouldRenderCommentButton" + ref="addNoteTooltipLeft" + v-gl-tooltip + class="add-diff-note tooltip-wrapper" + :title="addCommentTooltipLeft" + > + <button + v-show="shouldShowCommentButtonLeft" + ref="addDiffNoteButtonLeft" + type="button" + class="add-diff-note note-button js-add-diff-note-button qa-diff-comment" + :disabled="line.left.commentsDisabled" + @click="handleCommentButton(line.left)" + > + <gl-icon :size="12" name="comment" /> + </button> + </span> + <a + v-if="line.left.old_line" + ref="lineNumberRefOld" + :data-linenumber="line.left.old_line" + :href="lineHrefOld" + @click="setHighlightedRow(lineCode)" + > + </a> + <diff-gutter-avatars + v-if="hasDiscussionsLeft" + :discussions="line.left.discussions" + :discussions-expanded="line.left.discussionsExpanded" + @toggleLineDiscussions=" + toggleLineDiscussions({ + lineCode: line.left.line_code, + fileHash, + expanded: !line.left.discussionsExpanded, + }) + " + /> + </td> <td :class="parallelViewLeftLineType" class="line-coverage left-side"></td> <td :id="line.left.line_code" + v-safe-html="line.left.rich_text" :class="parallelViewLeftLineType" class="line_content with-coverage parallel left-side" @mousedown="handleParallelLineMouseDown" - v-html="line.left.rich_text" ></td> </template> <template v-else> @@ -172,18 +353,46 @@ export default { <td class="line_content with-coverage parallel left-side empty-cell"></td> </template> <template v-if="line.right && !isMatchLineRight"> - <diff-table-cell - :file-hash="fileHash" - :line="line.right" - :line-type="newLineType" - :is-bottom="isBottom" - :is-hover="isRightHover" - :is-highlighted="isHighlighted" - :show-comment-button="true" - :diff-view-type="parallelDiffViewType" - line-position="right" - class="diff-line-num new_line" - /> + <td ref="newTd" :class="classNameMapCellRight" class="diff-line-num new_line"> + <span + v-if="shouldRenderCommentButton" + ref="addNoteTooltipRight" + v-gl-tooltip + class="add-diff-note tooltip-wrapper" + :title="addCommentTooltipRight" + > + <button + v-show="shouldShowCommentButtonRight" + ref="addDiffNoteButtonRight" + type="button" + class="add-diff-note note-button js-add-diff-note-button qa-diff-comment" + :disabled="line.right.commentsDisabled" + @click="handleCommentButton(line.right)" + > + <gl-icon :size="12" name="comment" /> + </button> + </span> + <a + v-if="line.right.new_line" + ref="lineNumberRefNew" + :data-linenumber="line.right.new_line" + :href="lineHrefNew" + @click="setHighlightedRow(lineCode)" + > + </a> + <diff-gutter-avatars + v-if="hasDiscussionsRight" + :discussions="line.right.discussions" + :discussions-expanded="line.right.discussionsExpanded" + @toggleLineDiscussions=" + toggleLineDiscussions({ + lineCode: line.right.line_code, + fileHash, + expanded: !line.right.discussionsExpanded, + }) + " + /> + </td> <td v-gl-tooltip.hover :title="coverageState.text" @@ -192,6 +401,7 @@ export default { ></td> <td :id="line.right.line_code" + v-safe-html="line.right.rich_text" :class="[ line.right.type, { @@ -200,7 +410,6 @@ export default { ]" class="line_content with-coverage parallel right-side" @mousedown="handleParallelLineMouseDown" - v-html="line.right.rich_text" ></td> </template> <template v-else> diff --git a/app/assets/javascripts/diffs/components/settings_dropdown.vue b/app/assets/javascripts/diffs/components/settings_dropdown.vue index c45de481a17..78647065c8e 100644 --- a/app/assets/javascripts/diffs/components/settings_dropdown.vue +++ b/app/assets/javascripts/diffs/components/settings_dropdown.vue @@ -1,17 +1,24 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; -import { GlDeprecatedButton } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlButtonGroup, GlButton, GlDropdown } from '@gitlab/ui'; +import { __ } from '~/locale'; export default { components: { - GlDeprecatedButton, - Icon, + GlButtonGroup, + GlButton, + GlDropdown, }, computed: { ...mapGetters('diffs', ['isInlineView', 'isParallelView']), ...mapState('diffs', ['renderTreeList', 'showWhitespace']), }, + mounted() { + this.patchAriaLabel(); + }, + updated() { + this.patchAriaLabel(); + }, methods: { ...mapActions('diffs', [ 'setInlineDiffViewType', @@ -19,74 +26,69 @@ export default { 'setRenderTreeList', 'setShowWhitespace', ]), + patchAriaLabel() { + this.$el + .querySelector('.js-show-diff-settings') + .setAttribute('aria-label', __('Diff view settings')); + }, }, }; </script> <template> - <div class="dropdown"> - <button - type="button" - class="btn btn-default js-show-diff-settings" - data-toggle="dropdown" - data-display="static" - > - <icon name="settings" /> <icon name="chevron-down" /> - </button> - <div class="dropdown-menu dropdown-menu-right p-2 pt-3 pb-3"> - <div> - <span class="bold d-block mb-1">{{ __('File browser') }}</span> - <div class="btn-group d-flex"> - <gl-deprecated-button - :class="{ active: !renderTreeList }" - class="w-100 js-list-view" - @click="setRenderTreeList(false)" - > - {{ __('List view') }} - </gl-deprecated-button> - <gl-deprecated-button - :class="{ active: renderTreeList }" - class="w-100 js-tree-view" - @click="setRenderTreeList(true)" - > - {{ __('Tree view') }} - </gl-deprecated-button> - </div> - </div> - <div class="mt-2"> - <span class="bold d-block mb-1">{{ __('Compare changes') }}</span> - <div class="btn-group d-flex js-diff-view-buttons"> - <gl-deprecated-button - id="inline-diff-btn" - :class="{ active: isInlineView }" - class="w-100 js-inline-diff-button" - data-view-type="inline" - @click="setInlineDiffViewType" - > - {{ __('Inline') }} - </gl-deprecated-button> - <gl-deprecated-button - id="parallel-diff-btn" - :class="{ active: isParallelView }" - class="w-100 js-parallel-diff-button" - data-view-type="parallel" - @click="setParallelDiffViewType" - > - {{ __('Side-by-side') }} - </gl-deprecated-button> - </div> - </div> - <div class="mt-2"> - <label class="mb-0"> - <input - id="show-whitespace" - type="checkbox" - :checked="showWhitespace" - @change="setShowWhitespace({ showWhitespace: $event.target.checked, pushState: true })" - /> - {{ __('Show whitespace changes') }} - </label> - </div> + <gl-dropdown icon="settings" toggle-class="js-show-diff-settings" right> + <div class="gl-px-3"> + <span class="gl-font-weight-bold gl-display-block gl-mb-2">{{ __('File browser') }}</span> + <gl-button-group class="gl-display-flex"> + <gl-button + :class="{ selected: !renderTreeList }" + class="gl-w-half js-list-view" + @click="setRenderTreeList(false)" + > + {{ __('List view') }} + </gl-button> + <gl-button + :class="{ selected: renderTreeList }" + class="gl-w-half js-tree-view" + @click="setRenderTreeList(true)" + > + {{ __('Tree view') }} + </gl-button> + </gl-button-group> + </div> + <div class="gl-mt-3 gl-px-3"> + <span class="gl-font-weight-bold gl-display-block gl-mb-2">{{ __('Compare changes') }}</span> + <gl-button-group class="gl-display-flex js-diff-view-buttons"> + <gl-button + id="inline-diff-btn" + :class="{ selected: isInlineView }" + class="gl-w-half js-inline-diff-button" + data-view-type="inline" + @click="setInlineDiffViewType" + > + {{ __('Inline') }} + </gl-button> + <gl-button + id="parallel-diff-btn" + :class="{ selected: isParallelView }" + class="gl-w-half js-parallel-diff-button" + data-view-type="parallel" + @click="setParallelDiffViewType" + > + {{ __('Side-by-side') }} + </gl-button> + </gl-button-group> + </div> + <div class="gl-mt-3 gl-px-3"> + <label class="gl-mb-0"> + <input + id="show-whitespace" + type="checkbox" + :checked="showWhitespace" + @change="setShowWhitespace({ showWhitespace: $event.target.checked, pushState: true })" + /> + {{ __('Show whitespace changes') }} + </label> </div> - </div> + </gl-dropdown> </template> diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index 38fbd8e61d4..d03d450b12d 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -1,8 +1,7 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; -import { GlTooltipDirective } from '@gitlab/ui'; +import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; import FileTree from '~/vue_shared/components/file_tree.vue'; import DiffFileRow from './diff_file_row.vue'; @@ -11,7 +10,7 @@ export default { GlTooltip: GlTooltipDirective, }, components: { - Icon, + GlIcon, FileTree, }, props: { @@ -26,7 +25,7 @@ export default { }; }, computed: { - ...mapState('diffs', ['tree', 'renderTreeList', 'currentDiffFileId']), + ...mapState('diffs', ['tree', 'renderTreeList', 'currentDiffFileId', 'viewedDiffFileIds']), ...mapGetters('diffs', ['allBlobs']), filteredTreeList() { const search = this.search.toLowerCase().trim(); @@ -66,7 +65,7 @@ export default { <div class="tree-list-holder d-flex flex-column"> <div class="gl-mb-3 position-relative tree-list-search d-flex"> <div class="flex-fill d-flex"> - <icon name="search" class="position-absolute tree-list-icon" /> + <gl-icon name="search" class="position-absolute tree-list-icon" /> <label for="diff-tree-search" class="sr-only">{{ $options.searchPlaceholder }}</label> <input id="diff-tree-search" @@ -83,7 +82,7 @@ export default { class="position-absolute bg-transparent tree-list-icon tree-list-clear-icon border-0 p-0" @click="clearSearch" > - <icon name="close" /> + <gl-icon name="close" /> </button> </div> </div> @@ -94,6 +93,7 @@ export default { :key="file.key" :file="file" :level="0" + :viewed-files="viewedDiffFileIds" :hide-file-stats="hideFileStats" :file-row-component="$options.DiffFileRow" :current-diff-file-id="currentDiffFileId" diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index 447136036ee..dc97d9993da 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -34,7 +34,6 @@ export const COUNT_OF_AVATARS_IN_GUTTER = 3; export const LENGTH_OF_AVATAR_TOOLTIP = 17; export const LINES_TO_BE_RENDERED_DIRECTLY = 100; -export const MAX_LINES_TO_BE_RENDERED = 2000; export const DIFF_FILE_SYMLINK_MODE = '120000'; export const DIFF_FILE_DELETED_MODE = '0'; @@ -69,6 +68,11 @@ export const DIFFS_PER_PAGE = 20; export const DIFF_COMPARE_BASE_VERSION_INDEX = -1; export const DIFF_COMPARE_HEAD_VERSION_INDEX = -2; +// Diff View Alerts +export const ALERT_OVERFLOW_HIDDEN = 'overflow'; +export const ALERT_MERGE_CONFLICT = 'merge-conflict'; +export const ALERT_COLLAPSED_FILES = 'collapsed'; + // State machine states export const STATE_IDLING = 'idle'; export const STATE_LOADING = 'loading'; diff --git a/app/assets/javascripts/diffs/diff_file.js b/app/assets/javascripts/diffs/diff_file.js index 717c4a79ef9..610b71235d9 100644 --- a/app/assets/javascripts/diffs/diff_file.js +++ b/app/assets/javascripts/diffs/diff_file.js @@ -18,7 +18,6 @@ function fileSymlinkInformation(file, fileList) { ); } -/* eslint-disable-next-line import/prefer-default-export */ export function prepareRawDiffFile({ file, allFiles }) { Object.assign(file, { brokenSymlink: fileSymlinkInformation(file, allFiles), diff --git a/app/assets/javascripts/diffs/i18n.js b/app/assets/javascripts/diffs/i18n.js new file mode 100644 index 00000000000..8b91543587c --- /dev/null +++ b/app/assets/javascripts/diffs/i18n.js @@ -0,0 +1,14 @@ +import { __ } from '~/locale'; + +export const GENERIC_ERROR = __('Something went wrong on our end. Please try again!'); + +export const DIFF_FILE = { + blobView: __('You can %{linkStart}view the blob%{linkEnd} instead.'), + editInFork: __( + "You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.", + ), + fork: __('Fork'), + cancel: __('Cancel'), + collapsed: __('This file is collapsed.'), + expand: __('Expand file'), +}; diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index d5581474c9b..0f275f1cb3e 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -52,7 +52,6 @@ export const setBaseConfig = ({ commit }, options) => { projectPath, dismissEndpoint, showSuggestPopover, - useSingleDiffStyle, } = options; commit(types.SET_BASE_CONFIG, { endpoint, @@ -62,61 +61,18 @@ export const setBaseConfig = ({ commit }, options) => { projectPath, dismissEndpoint, showSuggestPopover, - useSingleDiffStyle, }); }; -export const fetchDiffFiles = ({ state, commit }) => { - const worker = new TreeWorker(); - const urlParams = { - w: state.showWhitespace ? '0' : '1', - }; - let returnData; - - if (state.useSingleDiffStyle) { - urlParams.view = state.diffViewType; - } - - commit(types.SET_LOADING, true); - - worker.addEventListener('message', ({ data }) => { - commit(types.SET_TREE_DATA, data); - - worker.terminate(); - }); - - return axios - .get(mergeUrlParams(urlParams, state.endpoint)) - .then(res => { - commit(types.SET_LOADING, false); - - commit(types.SET_MERGE_REQUEST_DIFFS, res.data.merge_request_diffs || []); - commit(types.SET_DIFF_DATA, res.data); - - worker.postMessage(state.diffFiles); - - returnData = res.data; - return Vue.nextTick(); - }) - .then(() => { - handleLocationHash(); - return returnData; - }) - .catch(() => worker.terminate()); -}; - export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => { const id = window?.location?.hash; const isNoteLink = id.indexOf('#note') === 0; const urlParams = { per_page: DIFFS_PER_PAGE, w: state.showWhitespace ? '0' : '1', + view: window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType, }; - if (state.useSingleDiffStyle) { - urlParams.view = state.diffViewType; - } - commit(types.SET_BATCH_LOADING, true); commit(types.SET_RETRIEVING_BATCHES, true); @@ -128,7 +84,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => { commit(types.SET_BATCH_LOADING, false); if (!isNoteLink && !state.currentDiffFileId) { - commit(types.UPDATE_CURRENT_DIFF_FILE_ID, diff_files[0].file_hash); + commit(types.VIEW_DIFF_FILE, diff_files[0].file_hash); } if (isNoteLink) { @@ -144,7 +100,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => { !state.diffFiles.some(f => f.file_hash === state.currentDiffFileId) && !isNoteLink ) { - commit(types.UPDATE_CURRENT_DIFF_FILE_ID, state.diffFiles[0].file_hash); + commit(types.VIEW_DIFF_FILE, state.diffFiles[0].file_hash); } if (gon.features?.codeNavigation) { @@ -175,11 +131,9 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => { export const fetchDiffFilesMeta = ({ commit, state }) => { const worker = new TreeWorker(); - const urlParams = {}; - - if (state.useSingleDiffStyle) { - urlParams.view = state.diffViewType; - } + const urlParams = { + view: window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType, + }; commit(types.SET_LOADING, true); @@ -229,7 +183,7 @@ export const fetchCoverageFiles = ({ commit, state }) => { export const setHighlightedRow = ({ commit }, lineCode) => { const fileHash = lineCode.split('_')[0]; commit(types.SET_HIGHLIGHTED_ROW, lineCode); - commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash); + commit(types.VIEW_DIFF_FILE, fileHash); }; // This is adding line discussions to the actual lines in the diff tree @@ -240,10 +194,7 @@ export const assignDiscussionsToDiff = ( ) => { const id = window?.location?.hash; const isNoteLink = id.indexOf('#note') === 0; - const diffPositionByLineCode = getDiffPositionByLineCode( - state.diffFiles, - state.useSingleDiffStyle, - ); + const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles); const hash = getLocationHash(); discussions @@ -477,13 +428,17 @@ export const toggleTreeOpen = ({ commit }, path) => { commit(types.TOGGLE_FOLDER_OPEN, path); }; +export const toggleActiveFileByHash = ({ commit }, hash) => { + commit(types.VIEW_DIFF_FILE, hash); +}; + export const scrollToFile = ({ state, commit }, path) => { if (!state.treeEntries[path]) return; const { fileHash } = state.treeEntries[path]; document.location.hash = fileHash; - commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash); + commit(types.VIEW_DIFF_FILE, fileHash); }; export const toggleShowTreeList = ({ commit, state }, saving = true) => { @@ -751,7 +706,7 @@ export const setCurrentDiffFileIdFromNote = ({ commit, state, rootGetters }, not const fileHash = rootGetters.getDiscussion(note.discussion_id).diff_file?.file_hash; if (fileHash && state.diffFiles.some(f => f.file_hash === fileHash)) { - commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash); + commit(types.VIEW_DIFF_FILE, fileHash); } }; @@ -759,5 +714,5 @@ export const navigateToDiffFileIndex = ({ commit, state }, index) => { const fileHash = state.diffFiles[index].file_hash; document.location.hash = fileHash; - commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash); + commit(types.VIEW_DIFF_FILE, fileHash); }; diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js index a24894b8d6b..42df5873a41 100644 --- a/app/assets/javascripts/diffs/store/getters.js +++ b/app/assets/javascripts/diffs/store/getters.js @@ -1,4 +1,5 @@ import { __, n__ } from '~/locale'; +import { parallelizeDiffLines } from './utils'; import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants'; export * from './getters_versions_dropdowns'; @@ -129,3 +130,11 @@ export const fileLineCoverage = state => (file, line) => { */ export const currentDiffIndex = state => Math.max(0, state.diffFiles.findIndex(diff => diff.file_hash === state.currentDiffFileId)); + +export const diffLines = state => file => { + if (state.diffViewType === INLINE_DIFF_VIEW_TYPE) { + return null; + } + + return parallelizeDiffLines(file.highlighted_diff_lines || []); +}; diff --git a/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js index 1e8e736c028..135b1c61ef5 100644 --- a/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js +++ b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js @@ -11,17 +11,26 @@ export const diffCompareDropdownTargetVersions = (state, getters) => { // startVersion only exists if the user has selected a version other // than "base" so if startVersion is null then base must be selected - const diffHead = parseBoolean(getParameterByName('diff_head')); + const defaultMergeRefForDiffs = window.gon?.features?.defaultMergeRefForDiffs || false; + const diffHeadParam = getParameterByName('diff_head'); + const diffHead = parseBoolean(diffHeadParam) || (!diffHeadParam && defaultMergeRefForDiffs); const isBaseSelected = !state.startVersion && !diffHead; const isHeadSelected = !state.startVersion && diffHead; + let baseVersion = null; - const baseVersion = { - versionName: state.targetBranchName, - version_index: DIFF_COMPARE_BASE_VERSION_INDEX, - href: state.mergeRequestDiff.base_version_path, - isBase: true, - selected: isBaseSelected, - }; + if ( + !defaultMergeRefForDiffs || + (defaultMergeRefForDiffs && !state.mergeRequestDiff.head_version_path) + ) { + baseVersion = { + versionName: state.targetBranchName, + version_index: DIFF_COMPARE_BASE_VERSION_INDEX, + href: state.mergeRequestDiff.base_version_path, + isBase: true, + selected: + isBaseSelected || (defaultMergeRefForDiffs && !state.mergeRequestDiff.head_version_path), + }; + } const headVersion = { versionName: state.targetBranchName, @@ -40,7 +49,11 @@ export const diffCompareDropdownTargetVersions = (state, getters) => { }; }; - return [...state.mergeRequestDiffs.slice(1).map(formatVersion), baseVersion, headVersion]; + return [ + ...state.mergeRequestDiffs.slice(1).map(formatVersion), + baseVersion, + state.mergeRequestDiff.head_version_path && headVersion, + ].filter(a => a); }; export const diffCompareDropdownSourceVersions = (state, getters) => { diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index d31a600e354..001d9d9f594 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -34,6 +34,7 @@ export default () => ({ showTreeList: true, currentDiffFileId: '', projectPath: '', + viewedDiffFileIds: {}, commentForms: [], highlightedRow: null, renderTreeList: true, @@ -41,5 +42,4 @@ export default () => ({ fileFinderVisible: false, dismissEndpoint: '', showSuggestPopover: true, - useSingleDiffStyle: false, }); diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index 4b1dbc34902..5dba2e9d10d 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -19,7 +19,7 @@ export const SET_LINE_DISCUSSIONS_FOR_FILE = 'SET_LINE_DISCUSSIONS_FOR_FILE'; export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FILE'; export const TOGGLE_FOLDER_OPEN = 'TOGGLE_FOLDER_OPEN'; export const TOGGLE_SHOW_TREE_LIST = 'TOGGLE_SHOW_TREE_LIST'; -export const UPDATE_CURRENT_DIFF_FILE_ID = 'UPDATE_CURRENT_DIFF_FILE_ID'; +export const VIEW_DIFF_FILE = 'VIEW_DIFF_FILE'; export const OPEN_DIFF_FILE_COMMENT_FORM = 'OPEN_DIFF_FILE_COMMENT_FORM'; export const UPDATE_DIFF_FILE_COMMENT_FORM = 'UPDATE_DIFF_FILE_COMMENT_FORM'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 0d41f1c2178..7925c620c4e 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -1,4 +1,6 @@ +import Vue from 'vue'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { INLINE_DIFF_VIEW_TYPE } from '../constants'; import { findDiffFile, addLineReferences, @@ -24,7 +26,6 @@ export default { projectPath, dismissEndpoint, showSuggestPopover, - useSingleDiffStyle, } = options; Object.assign(state, { endpoint, @@ -34,7 +35,6 @@ export default { projectPath, dismissEndpoint, showSuggestPopover, - useSingleDiffStyle, }); }, @@ -57,10 +57,7 @@ export default { [types.SET_DIFF_DATA](state, data) { let files = state.diffFiles; - if ( - !(gon?.features?.diffsBatchLoad && window.location.search.indexOf('diff_id') === -1) && - data.diff_files - ) { + if (window.location.search.indexOf('diff_id') !== -1 && data.diff_files) { files = prepareDiffData(data, files); } @@ -154,7 +151,9 @@ export default { addContextLines({ inlineLines: diffFile.highlighted_diff_lines, parallelLines: diffFile.parallel_diff_lines, - diffViewType: state.diffViewType, + diffViewType: window.gon?.features?.unifiedDiffLines + ? INLINE_DIFF_VIEW_TYPE + : state.diffViewType, contextLines: lines, bottom, lineNumbers, @@ -249,7 +248,7 @@ export default { }); } - if (!file.parallel_diff_lines || !file.highlighted_diff_lines) { + if (!file.parallel_diff_lines.length || !file.highlighted_diff_lines.length) { const newDiscussions = (file.discussions || []) .filter(d => d.id !== discussion.id) .concat(discussion); @@ -293,8 +292,9 @@ export default { [types.TOGGLE_SHOW_TREE_LIST](state) { state.showTreeList = !state.showTreeList; }, - [types.UPDATE_CURRENT_DIFF_FILE_ID](state, fileId) { + [types.VIEW_DIFF_FILE](state, fileId) { state.currentDiffFileId = fileId; + Vue.set(state.viewedDiffFileIds, fileId, true); }, [types.OPEN_DIFF_FILE_COMMENT_FORM](state, formData) { state.commentForms.push({ diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index f014cddda32..69330ffae2f 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -11,7 +11,6 @@ import { OLD_LINE_TYPE, MATCH_LINE_TYPE, LINES_TO_BE_RENDERED_DIRECTLY, - MAX_LINES_TO_BE_RENDERED, TREE_TYPE, INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE, @@ -20,6 +19,77 @@ import { } from '../constants'; import { prepareRawDiffFile } from '../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); + +/** + * Pass in the inline diff lines array which gets converted + * to the parallel diff lines. + * This allows for us to convert inline diff lines to parallel + * on the frontend without needing to send any requests + * to the API. + * + * This method has been taken from the already existing backend + * implementation at lib/gitlab/diff/parallel_diff.rb + * + * @param {Object[]} diffLines - inline diff lines + * + * @returns {Object[]} parallel lines + */ +export const parallelizeDiffLines = (diffLines = []) => { + let freeRightIndex = null; + const lines = []; + + for (let i = 0, diffLinesLength = diffLines.length, index = 0; i < diffLinesLength; i += 1) { + const line = diffLines[i]; + + if (isRemoved(line)) { + lines.push({ + [LINE_POSITION_LEFT]: line, + [LINE_POSITION_RIGHT]: null, + }); + + if (freeRightIndex === null) { + // Once we come upon a new line it can be put on the right of this old line + freeRightIndex = index; + } + index += 1; + } else if (isAdded(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. + lines[freeRightIndex].right = line; + + // If there are any other old lines on the left that don't yet have + // a new counterpart on the right, update the free_right_index + const nextFreeRightIndex = freeRightIndex + 1; + freeRightIndex = nextFreeRightIndex < index ? nextFreeRightIndex : null; + } else { + lines.push({ + [LINE_POSITION_LEFT]: null, + [LINE_POSITION_RIGHT]: line, + }); + + 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, + }); + + freeRightIndex = null; + index += 1; + } + } + + return lines; +}; + export function findDiffFile(files, match, matchKey = 'file_hash') { return files.find(file => file[matchKey] === match); } @@ -281,7 +351,7 @@ function mergeTwoFiles(target, source) { function ensureBasicDiffFileLines(file) { const missingInline = !file.highlighted_diff_lines; - const missingParallel = !file.parallel_diff_lines; + const missingParallel = !file.parallel_diff_lines || window.gon?.features?.unifiedDiffLines; Object.assign(file, { highlighted_diff_lines: missingInline ? [] : file.highlighted_diff_lines, @@ -379,12 +449,10 @@ function getVisibleDiffLines(file) { } function finalizeDiffFile(file) { - const name = (file.viewer && file.viewer.name) || diffViewerModes.text; const lines = getVisibleDiffLines(file); Object.assign(file, { renderIt: lines < LINES_TO_BE_RENDERED_DIRECTLY, - collapsed: name === diffViewerModes.text && lines > MAX_LINES_TO_BE_RENDERED, isShowingFullFile: false, isLoadingFullFile: false, discussions: [], @@ -417,11 +485,11 @@ export function prepareDiffData(diff, priorFiles = []) { return deduplicateFilesList([...priorFiles, ...cleanedFiles]); } -export function getDiffPositionByLineCode(diffFiles, useSingleDiffStyle) { +export function getDiffPositionByLineCode(diffFiles) { let lines = []; const hasInlineDiffs = diffFiles.some(file => file.highlighted_diff_lines.length > 0); - if (!useSingleDiffStyle || hasInlineDiffs) { + if (hasInlineDiffs) { // In either of these cases, we can use `highlighted_diff_lines` because // that will include all of the parallel diff lines, too diff --git a/app/assets/javascripts/diffs/utils/uuids.js b/app/assets/javascripts/diffs/utils/uuids.js index 1a529c07ccc..12448350e62 100644 --- a/app/assets/javascripts/diffs/utils/uuids.js +++ b/app/assets/javascripts/diffs/utils/uuids.js @@ -11,9 +11,6 @@ * @typedef {String} UUIDv4 */ -// https://gitlab.com/gitlab-org/frontend/rfcs/-/issues/20 -/* eslint-disable import/prefer-default-export */ - import MersenneTwister from 'mersenne-twister'; import stringHash from 'string-hash'; import { isString } from 'lodash'; diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index 6ebbeecae1d..5674cc8495d 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -6,6 +6,7 @@ import { __ } from '~/locale'; import axios from './lib/utils/axios_utils'; import { timeFor, parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility'; import boardsStore from './boards/stores/boards_store'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; class DueDateSelect { constructor({ $dropdown, $loading } = {}) { @@ -35,7 +36,7 @@ class DueDateSelect { } initGlDropdown() { - this.$dropdown.glDropdown({ + initDeprecatedJQueryDropdown(this.$dropdown, { opened: () => { const calendar = this.$datePicker.data('pikaday'); calendar.show(); diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js new file mode 100644 index 00000000000..9ee692e953a --- /dev/null +++ b/app/assets/javascripts/editor/constants.js @@ -0,0 +1,7 @@ +import { __ } from '~/locale'; + +export const EDITOR_LITE_INSTANCE_ERROR_NO_EL = __( + '"el" parameter is required for createInstance()', +); + +export const URI_PREFIX = 'gitlab'; diff --git a/app/assets/javascripts/editor/editor_file_template_ext.js b/app/assets/javascripts/editor/editor_file_template_ext.js new file mode 100644 index 00000000000..343908b831d --- /dev/null +++ b/app/assets/javascripts/editor/editor_file_template_ext.js @@ -0,0 +1,7 @@ +import { Position } from 'monaco-editor'; + +export default { + navigateFileStart() { + this.setPosition(new Position(1, 1)); + }, +}; diff --git a/app/assets/javascripts/editor/editor_lite.js b/app/assets/javascripts/editor/editor_lite.js index 0af0c3ecdcf..bbd5461ae4d 100644 --- a/app/assets/javascripts/editor/editor_lite.js +++ b/app/assets/javascripts/editor/editor_lite.js @@ -1,18 +1,15 @@ -import { editor as monacoEditor, languages as monacoLanguages, Position, Uri } from 'monaco-editor'; +import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor'; import { DEFAULT_THEME, themes } from '~/ide/lib/themes'; import languages from '~/ide/lib/languages'; import { defaultEditorOptions } from '~/ide/lib/editor_options'; import { registerLanguages } from '~/ide/utils'; import { joinPaths } from '~/lib/utils/url_utility'; import { clearDomElement } from './utils'; +import { EDITOR_LITE_INSTANCE_ERROR_NO_EL, URI_PREFIX } from './constants'; export default class Editor { constructor(options = {}) { - this.editorEl = null; - this.blobContent = ''; - this.blobPath = ''; - this.instance = null; - this.model = null; + this.instances = []; this.options = { extraEditorClassName: 'gl-editor-lite', ...defaultEditorOptions, @@ -31,6 +28,17 @@ export default class Editor { monacoEditor.setTheme(theme ? themeName : DEFAULT_THEME); } + static updateModelLanguage(path, instance) { + if (!instance) return; + const model = instance.getModel(); + const ext = `.${path.split('.').pop()}`; + const language = monacoLanguages + .getLanguages() + .find(lang => lang.extensions.indexOf(ext) !== -1); + const id = language ? language.id : 'plaintext'; + monacoEditor.setModelLanguage(model, id); + } + /** * Creates a monaco instance with the given options. * @@ -40,74 +48,53 @@ export default class Editor { * @param {string} options.blobContent The content to initialize the monacoEditor. * @param {string} options.blobGlobalId This is used to help globally identify monaco instances that are created with the same blobPath. */ - createInstance({ el = undefined, blobPath = '', blobContent = '', blobGlobalId = '' } = {}) { - if (!el) return; - this.editorEl = el; - this.blobContent = blobContent; - this.blobPath = blobPath; - - clearDomElement(this.editorEl); - - const uriFilePath = joinPaths('gitlab', blobGlobalId, blobPath); - - this.model = monacoEditor.createModel(this.blobContent, undefined, Uri.file(uriFilePath)); - - monacoEditor.onDidCreateEditor(this.renderEditor.bind(this)); - - this.instance = monacoEditor.create(this.editorEl, this.options); - this.instance.setModel(this.model); - } - - dispose() { - if (this.model) { - this.model.dispose(); - this.model = null; + createInstance({ + el = undefined, + blobPath = '', + blobContent = '', + blobGlobalId = '', + ...instanceOptions + } = {}) { + if (!el) { + throw new Error(EDITOR_LITE_INSTANCE_ERROR_NO_EL); } - return this.instance && this.instance.dispose(); - } + clearDomElement(el); - renderEditor() { - delete this.editorEl.dataset.editorLoading; - } + const uriFilePath = joinPaths(URI_PREFIX, blobGlobalId, blobPath); - onChangeContent(fn) { - return this.model.onDidChangeContent(fn); - } + const model = monacoEditor.createModel(blobContent, undefined, Uri.file(uriFilePath)); - updateModelLanguage(path) { - if (path === this.blobPath) return; - this.blobPath = path; - const ext = `.${path.split('.').pop()}`; - const language = monacoLanguages - .getLanguages() - .find(lang => lang.extensions.indexOf(ext) !== -1); - const id = language ? language.id : 'plaintext'; - monacoEditor.setModelLanguage(this.model, id); - } + monacoEditor.onDidCreateEditor(() => { + delete el.dataset.editorLoading; + }); - getValue() { - return this.instance.getValue(); - } - - setValue(val) { - this.instance.setValue(val); - } + const instance = monacoEditor.create(el, { + ...this.options, + ...instanceOptions, + }); + instance.setModel(model); + instance.onDidDispose(() => { + const index = this.instances.findIndex(inst => inst === instance); + this.instances.splice(index, 1); + model.dispose(); + }); + instance.updateModelLanguage = path => Editor.updateModelLanguage(path, instance); - focus() { - this.instance.focus(); + this.instances.push(instance); + return instance; } - navigateFileStart() { - this.instance.setPosition(new Position(1, 1)); - } - - updateOptions(options = {}) { - this.instance.updateOptions(options); + dispose() { + this.instances.forEach(instance => instance.dispose()); } - use(exts = []) { + use(exts = [], instance = null) { const extensions = Array.isArray(exts) ? exts : [exts]; - Object.assign(this, ...extensions); + if (instance) { + Object.assign(instance, ...extensions); + } else { + this.instances.forEach(inst => Object.assign(inst, ...extensions)); + } } } diff --git a/app/assets/javascripts/editor/editor_markdown_ext.js b/app/assets/javascripts/editor/editor_markdown_ext.js index 9d09663e643..c46f5736912 100644 --- a/app/assets/javascripts/editor/editor_markdown_ext.js +++ b/app/assets/javascripts/editor/editor_markdown_ext.js @@ -1,7 +1,7 @@ export default { getSelectedText(selection = this.getSelection()) { const { startLineNumber, endLineNumber, startColumn, endColumn } = selection; - const valArray = this.instance.getValue().split('\n'); + const valArray = this.getValue().split('\n'); let text = ''; if (startLineNumber === endLineNumber) { text = valArray[startLineNumber - 1].slice(startColumn - 1, endColumn - 1); @@ -20,20 +20,16 @@ export default { return text; }, - getSelection() { - return this.instance.getSelection(); - }, - replaceSelectedText(text, select = undefined) { const forceMoveMarkers = !select; - this.instance.executeEdits('', [{ range: this.getSelection(), text, forceMoveMarkers }]); + this.executeEdits('', [{ range: this.getSelection(), text, forceMoveMarkers }]); }, moveCursor(dx = 0, dy = 0) { - const pos = this.instance.getPosition(); + const pos = this.getPosition(); pos.column += dx; pos.lineNumber += dy; - this.instance.setPosition(pos); + this.setPosition(pos); }, /** @@ -94,6 +90,6 @@ export default { .setStartPosition(newStartLineNumber, newStartColumn) .setEndPosition(newEndLineNumber, newEndColumn); - this.instance.setSelection(newSelection); + this.setSelection(newSelection); }, }; diff --git a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue index f0723e96ddf..b2a8571820b 100644 --- a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue +++ b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ /** * Render modal to confirm rollback/redeploy. */ diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index d2978422224..035b276bc3b 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -1,8 +1,7 @@ <script> -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlButton, GlIcon, GlLoadingIcon } from '@gitlab/ui'; import { __, s__, sprintf } from '~/locale'; import { formatTime } from '~/lib/utils/datetime_utility'; -import Icon from '~/vue_shared/components/icon.vue'; import eventHub from '../event_hub'; import tooltip from '../../vue_shared/directives/tooltip'; @@ -11,7 +10,8 @@ export default { tooltip, }, components: { - Icon, + GlButton, + GlIcon, GlLoadingIcon, }, props: { @@ -69,38 +69,37 @@ export default { </script> <template> <div class="btn-group" role="group"> - <button + <gl-button v-tooltip :title="title" :aria-label="title" :disabled="isLoading" - type="button" - class="dropdown btn btn-default dropdown-new js-environment-actions-dropdown" + class="dropdown dropdown-new js-environment-actions-dropdown" data-container="body" data-toggle="dropdown" > <span> - <icon name="play" /> - <icon name="chevron-down" /> + <gl-icon name="play" /> + <gl-icon name="chevron-down" /> <gl-loading-icon v-if="isLoading" /> </span> - </button> + </gl-button> <ul class="dropdown-menu dropdown-menu-right"> - <li v-for="(action, i) in actions" :key="i"> - <button + <li v-for="(action, i) in actions" :key="i" class="gl-display-flex"> + <gl-button :class="{ disabled: isActionDisabled(action) }" :disabled="isActionDisabled(action)" - type="button" - class="js-manual-action-link no-btn btn d-flex align-items-center" + variant="link" + class="js-manual-action-link gl-flex-fill-1" @click="onClickAction(action)" > - <span class="flex-fill">{{ action.name }}</span> - <span v-if="action.scheduledAt" class="text-secondary"> - <icon name="clock" /> + <span class="gl-flex-fill-1">{{ action.name }}</span> + <span v-if="action.scheduledAt" class="text-secondary float-right"> + <gl-icon name="clock" /> {{ remainingTime(action) }} </span> - </button> + </gl-button> </li> </ul> </div> diff --git a/app/assets/javascripts/environments/components/environment_delete.vue b/app/assets/javascripts/environments/components/environment_delete.vue index b53c5fa6583..55aaa6d57bd 100644 --- a/app/assets/javascripts/environments/components/environment_delete.vue +++ b/app/assets/javascripts/environments/components/environment_delete.vue @@ -5,15 +5,14 @@ */ import $ from 'jquery'; -import { GlTooltipDirective } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { s__ } from '~/locale'; import eventHub from '../event_hub'; import LoadingButton from '../../vue_shared/components/loading_button.vue'; export default { components: { - Icon, + GlIcon, LoadingButton, }, directives: { @@ -65,6 +64,6 @@ export default { data-target="#delete-environment-modal" @click="onClick" > - <icon name="remove" /> + <gl-icon name="remove" /> </loading-button> </template> diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index fa3d217f148..8850ed19a4b 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -1,13 +1,12 @@ <script> /* eslint-disable @gitlab/vue-require-i18n-strings */ import { isEmpty } from 'lodash'; -import { GlTooltipDirective } from '@gitlab/ui'; +import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import timeagoMixin from '~/vue_shared/mixins/timeago'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import CommitComponent from '~/vue_shared/components/commit.vue'; -import Icon from '~/vue_shared/components/icon.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import eventHub from '../event_hub'; import ActionsComponent from './environment_actions.vue'; @@ -30,7 +29,7 @@ export default { ActionsComponent, CommitComponent, ExternalUrlComponent, - Icon, + GlIcon, MonitoringButtonComponent, PinComponent, DeleteComponent, @@ -535,7 +534,7 @@ export default { </div> <span v-if="shouldRenderDeployBoard" class="deploy-board-icon" @click="toggleDeployBoard"> - <icon :name="deployIconName" /> + <gl-icon :name="deployIconName" /> </span> <span @@ -560,9 +559,9 @@ export default { role="button" @click="onClickFolder" > - <icon :name="folderIconName" class="folder-icon" /> + <gl-icon :name="folderIconName" class="folder-icon" /> - <icon name="folder" class="folder-icon" /> + <gl-icon name="folder" class="folder-icon" /> <span> {{ model.folderName }} </span> diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue index bd6feb14319..4dc2c0ec1bd 100644 --- a/app/assets/javascripts/environments/components/environment_monitoring.vue +++ b/app/assets/javascripts/environments/components/environment_monitoring.vue @@ -1,15 +1,12 @@ <script> -import { GlDeprecatedButton, GlTooltipDirective } from '@gitlab/ui'; +import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import { __ } from '~/locale'; /** * Renders the Monitoring (Metrics) link in environments table. */ -import Icon from '~/vue_shared/components/icon.vue'; - export default { components: { - Icon, - GlDeprecatedButton, + GlButton, }, directives: { GlTooltip: GlTooltipDirective, @@ -28,15 +25,14 @@ export default { }; </script> <template> - <gl-deprecated-button + <gl-button v-gl-tooltip :href="monitoringUrl" :title="title" :aria-label="title" - class="monitoring-url d-none d-sm-none d-md-block" + class="monitoring-url gl-display-none gl-display-sm-none gl-display-md-block" + icon="chart" rel="noopener noreferrer nofollow" variant="default" - > - <icon name="chart" /> - </gl-deprecated-button> + /> </template> diff --git a/app/assets/javascripts/environments/components/environment_pin.vue b/app/assets/javascripts/environments/components/environment_pin.vue index 3a219e98b08..52ac7725bde 100644 --- a/app/assets/javascripts/environments/components/environment_pin.vue +++ b/app/assets/javascripts/environments/components/environment_pin.vue @@ -3,15 +3,14 @@ * Renders a prevent auto-stop button. * Used in environments table. */ -import { GlDeprecatedButton, GlTooltipDirective } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import eventHub from '../event_hub'; export default { components: { - Icon, - GlDeprecatedButton, + GlIcon, + GlButton, }, directives: { GlTooltip: GlTooltipDirective, @@ -31,12 +30,7 @@ export default { }; </script> <template> - <gl-deprecated-button - v-gl-tooltip - :title="$options.title" - :aria-label="$options.title" - @click="onPinClick" - > - <icon name="thumbtack" /> - </gl-deprecated-button> + <gl-button v-gl-tooltip :title="$options.title" :aria-label="$options.title" @click="onPinClick"> + <gl-icon name="thumbtack" /> + </gl-button> </template> diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue index 7a728961b37..32528e6c6ea 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.vue +++ b/app/assets/javascripts/environments/components/environment_rollback.vue @@ -5,21 +5,13 @@ * * Makes a post request when the button is clicked. */ -import { - GlTooltipDirective, - GlLoadingIcon, - GlModalDirective, - GlDeprecatedButton, -} from '@gitlab/ui'; +import { GlTooltipDirective, GlModalDirective, GlButton } from '@gitlab/ui'; import { s__ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; import eventHub from '../event_hub'; export default { components: { - Icon, - GlLoadingIcon, - GlDeprecatedButton, + GlButton, }, directives: { GlTooltip: GlTooltipDirective, @@ -73,15 +65,13 @@ export default { }; </script> <template> - <gl-deprecated-button + <gl-button v-gl-tooltip v-gl-modal.confirm-rollback-modal - :disabled="isLoading" + class="gl-display-none gl-display-md-block text-secondary" + :loading="isLoading" :title="title" - class="d-none d-md-block text-secondary" + :icon="isLastDeployment ? 'repeat' : 'redo'" @click="onClick" - > - <icon v-if="isLastDeployment" name="repeat" /> <icon v-else name="redo" /> - <gl-loading-icon v-if="isLoading" /> - </gl-deprecated-button> + /> </template> diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue index 37f94f9f5ab..b5a7be90204 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.vue +++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue @@ -3,13 +3,12 @@ * Renders a terminal button to open a web terminal. * Used in environments table. */ -import { GlTooltipDirective } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; export default { components: { - Icon, + GlIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -42,6 +41,6 @@ export default { :class="{ disabled: disabled }" class="btn terminal-button d-none d-sm-none d-md-block text-secondary" > - <icon name="terminal" /> + <gl-icon name="terminal" /> </a> </template> diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index 1bf705dcda2..c06ab265915 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -14,6 +14,7 @@ export default { DeployBoard: () => import('ee_component/environments/components/deploy_board_component.vue'), CanaryDeploymentCallout: () => import('ee_component/environments/components/canary_deployment_callout.vue'), + EnvironmentAlert: () => import('ee_component/environments/components/environment_alert.vue'), }, props: { environments: { @@ -111,6 +112,9 @@ export default { shouldShowCanaryCallout(env) { return env.showCanaryCallout && this.showCanaryDeploymentCallout; }, + shouldRenderAlert(env) { + return env?.has_opened_alert; + }, sortEnvironments(environments) { /* * The sorting algorithm should sort in the following priorities: @@ -185,6 +189,11 @@ export default { /> </div> </div> + <environment-alert + v-if="shouldRenderAlert(model)" + :key="`alert-row-${i}`" + :environment="model" + /> <template v-if="shouldRenderFolderContent(model)"> <div v-if="model.isLoadingFolderContent" :key="`loading-item-${i}`"> diff --git a/app/assets/javascripts/environments/components/stop_environment_modal.vue b/app/assets/javascripts/environments/components/stop_environment_modal.vue index 7448fd584c6..88612376b6e 100644 --- a/app/assets/javascripts/environments/components/stop_environment_modal.vue +++ b/app/assets/javascripts/environments/components/stop_environment_modal.vue @@ -1,5 +1,5 @@ <script> -/* eslint-disable @gitlab/vue-require-i18n-strings */ +/* eslint-disable @gitlab/vue-require-i18n-strings, vue/no-v-html */ import { GlTooltipDirective } from '@gitlab/ui'; import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue'; import { s__, sprintf } from '~/locale'; diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js index 56896ac4d43..6c547c3713a 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js +++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js @@ -1,20 +1,33 @@ 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'; +import createDefaultClient from '~/lib/graphql'; Vue.use(Translate); +Vue.use(VueApollo); -export default () => - new Vue({ - el: '#environments-folder-list-view', +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), +}); + +export default () => { + const el = document.getElementById('environments-folder-list-view'); + + return new Vue({ + el, components: { environmentsFolderApp, }, mixins: [canaryCalloutMixin], + apolloProvider, + provide: { + projectPath: el.dataset.projectPath, + }, data() { - const environmentsData = document.querySelector(this.$options.el).dataset; + const environmentsData = el.dataset; return { endpoint: environmentsData.environmentsDataEndpoint, @@ -35,3 +48,4 @@ export default () => }); }, }); +}; diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue index e1e356a977f..16d25615779 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.vue +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -23,7 +23,8 @@ export default { }, cssContainerClass: { type: String, - required: true, + required: false, + default: '', }, canReadEnvironment: { type: Boolean, diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js index 4848cb0f13d..8e8af3f32f7 100644 --- a/app/assets/javascripts/environments/index.js +++ b/app/assets/javascripts/environments/index.js @@ -1,20 +1,32 @@ 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'; +import createDefaultClient from '~/lib/graphql'; Vue.use(Translate); +Vue.use(VueApollo); -export default () => - new Vue({ - el: '#environments-list-view', +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), +}); + +export default () => { + const el = document.getElementById('environments-list-view'); + return new Vue({ + el, components: { environmentsComponent, }, mixins: [canaryCalloutMixin], + apolloProvider, + provide: { + projectPath: el.dataset.projectPath, + }, data() { - const environmentsData = document.querySelector(this.$options.el).dataset; + const environmentsData = el.dataset; return { endpoint: environmentsData.environmentsDataEndpoint, @@ -39,3 +51,4 @@ export default () => }); }, }); +}; diff --git a/app/assets/javascripts/environments/mixins/canary_callout_mixin.js b/app/assets/javascripts/environments/mixins/canary_callout_mixin.js index 398576a31cb..e9f1a144cb3 100644 --- a/app/assets/javascripts/environments/mixins/canary_callout_mixin.js +++ b/app/assets/javascripts/environments/mixins/canary_callout_mixin.js @@ -2,7 +2,7 @@ import { parseBoolean } from '~/lib/utils/common_utils'; export default { data() { - const data = document.querySelector(this.$options.el).dataset; + const data = this.$options.el.dataset; return { canaryDeploymentFeatureId: data.environmentsDataCanaryDeploymentFeatureId, diff --git a/app/assets/javascripts/environments/stores/helpers.js b/app/assets/javascripts/environments/stores/helpers.js index 8eba6c00601..eb47ba29412 100644 --- a/app/assets/javascripts/environments/stores/helpers.js +++ b/app/assets/javascripts/environments/stores/helpers.js @@ -4,5 +4,4 @@ * @param {Object} environment * @returns {Object} */ -// eslint-disable-next-line import/prefer-default-export export const setDeployBoard = (oldEnvironmentState, environment) => environment; diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue index 3d1fdc4f168..5dee3ef3ffe 100644 --- a/app/assets/javascripts/error_tracking/components/error_details.vue +++ b/app/assets/javascripts/error_tracking/components/error_details.vue @@ -11,10 +11,10 @@ import { GlDeprecatedDropdown, GlDeprecatedDropdownItem, GlDeprecatedDropdownDivider, + GlIcon, } from '@gitlab/ui'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { __, sprintf, n__ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import Stacktrace from './stacktrace.vue'; import TrackEventDirective from '~/vue_shared/directives/track_event'; @@ -38,7 +38,7 @@ export default { GlLink, GlLoadingIcon, TooltipOnTruncate, - Icon, + GlIcon, Stacktrace, GlBadge, GlAlert, @@ -397,7 +397,7 @@ export default { data-testid="external-url-link" > <span class="text-truncate">{{ error.externalUrl }}</span> - <icon name="external-link" class="ml-1 flex-shrink-0" /> + <gl-icon name="external-link" class="ml-1 flex-shrink-0" /> </gl-link> </li> <li v-if="error.firstReleaseVersion"> diff --git a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue index c6825d7af45..a4938fe13ed 100644 --- a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue +++ b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue @@ -1,14 +1,14 @@ <script> -import { GlTooltip, GlSprintf } from '@gitlab/ui'; +/* eslint-disable vue/no-v-html */ +import { GlTooltip, GlSprintf, GlIcon } from '@gitlab/ui'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; -import Icon from '~/vue_shared/components/icon.vue'; export default { components: { ClipboardButton, FileIcon, - Icon, + GlIcon, GlSprintf, }, directives: { @@ -80,7 +80,7 @@ export default { <div ref="header" class="file-title file-title-flex-parent"> <div class="file-header-content d-flex align-content-center"> <div v-if="hasCode" class="d-inline-block cursor-pointer" @click="toggle()"> - <icon :name="collapseIcon" :size="16" aria-hidden="true" class="gl-mr-2" /> + <gl-icon :name="collapseIcon" :size="16" aria-hidden="true" class="gl-mr-2" /> </div> <file-icon :file-name="filePath" :size="18" aria-hidden="true" css-classes="gl-mr-2" /> <strong diff --git a/app/assets/javascripts/error_tracking/store/details/actions.js b/app/assets/javascripts/error_tracking/store/details/actions.js index 28806b3915c..df5be5224a7 100644 --- a/app/assets/javascripts/error_tracking/store/details/actions.js +++ b/app/assets/javascripts/error_tracking/store/details/actions.js @@ -10,7 +10,6 @@ const stopPolling = poll => { if (poll) poll.stop(); }; -// eslint-disable-next-line import/prefer-default-export export function startPollingStacktrace({ commit }, endpoint) { stackTracePoll = new Poll({ resource: service, diff --git a/app/assets/javascripts/error_tracking/store/details/getters.js b/app/assets/javascripts/error_tracking/store/details/getters.js index f2778fbb2c7..a3b31436c81 100644 --- a/app/assets/javascripts/error_tracking/store/details/getters.js +++ b/app/assets/javascripts/error_tracking/store/details/getters.js @@ -1,4 +1,3 @@ -// eslint-disable-next-line import/prefer-default-export export const stacktrace = state => state.stacktraceData.stack_trace_entries ? state.stacktraceData.stack_trace_entries.reverse() diff --git a/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue index 0de67a8bcc7..f1fb1a44758 100644 --- a/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue +++ b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue @@ -1,11 +1,10 @@ <script> import { mapActions, mapState } from 'vuex'; -import { GlFormInput } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlFormInput, GlIcon } from '@gitlab/ui'; import LoadingButton from '~/vue_shared/components/loading_button.vue'; export default { - components: { GlFormInput, Icon, LoadingButton }, + components: { GlFormInput, GlIcon, LoadingButton }, computed: { ...mapState(['apiHost', 'connectError', 'connectSuccessful', 'isLoadingProjects', 'token']), tokenInputState() { @@ -64,7 +63,7 @@ export default { :loading="isLoadingProjects" @click="fetchProjects" /> - <icon + <gl-icon v-show="connectSuccessful" class="js-error-tracking-connect-success gl-ml-2 text-success align-middle" :aria-label="__('Projects Successfully Retrieved')" 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 9bea7aa7b04..d2ac80fa190 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 @@ -86,6 +86,11 @@ export const conditions = flattenDeep( value: __('Any'), }, { + url: 'author_username=support-bot', + tokenKey: 'author', + value: 'support-bot', + }, + { url: 'milestone_title=None', tokenKey: 'milestone', value: __('None'), diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index 7697d97cb2c..f2dd8d5ace5 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -175,4 +175,4 @@ export { removeFlashClickListener, FLASH_TYPES, }; -window.Flash = deprecatedCreateFlash; +window.Flash = createFlash; diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue index c0dadedbc51..1203f389931 100644 --- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue +++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue @@ -1,5 +1,5 @@ <script> -/* eslint-disable vue/require-default-prop */ +/* eslint-disable vue/require-default-prop, vue/no-v-html */ import Identicon from '~/vue_shared/components/identicon.vue'; import highlight from '~/lib/utils/highlight'; import { truncateNamespace } from '~/lib/utils/text_utility'; diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue index 40add09f25d..19cb09f0dcc 100644 --- a/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue +++ b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue @@ -1,13 +1,13 @@ <script> import { debounce } from 'lodash'; import { mapActions } from 'vuex'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import eventHub from '../event_hub'; import frequentItemsMixin from './frequent_items_mixin'; export default { components: { - Icon, + GlIcon, }, mixins: [frequentItemsMixin], data() { @@ -49,6 +49,6 @@ export default { type="search" class="form-control" /> - <icon v-if="!searchQuery" name="search" class="search-icon" /> + <gl-icon v-if="!searchQuery" name="search" class="search-icon" /> </div> </template> diff --git a/app/assets/javascripts/frequent_items/index.js b/app/assets/javascripts/frequent_items/index.js index c074f173776..1998bf4358a 100644 --- a/app/assets/javascripts/frequent_items/index.js +++ b/app/assets/javascripts/frequent_items/index.js @@ -28,8 +28,8 @@ export default function initFrequentItemDropdowns() { return; } - $(navEl).on('shown.bs.dropdown', () => - import('./components/app.vue').then(({ default: FrequentItems }) => { + import('./components/app.vue') + .then(({ default: FrequentItems }) => { // eslint-disable-next-line no-new new Vue({ el, @@ -59,9 +59,11 @@ export default function initFrequentItemDropdowns() { }); }, }); + }) + .catch(() => {}); - eventHub.$emit(`${namespace}-dropdownOpen`); - }), - ); + $(navEl).on('shown.bs.dropdown', () => { + eventHub.$emit(`${namespace}-dropdownOpen`); + }); }); } diff --git a/app/assets/javascripts/frequent_items/store/getters.js b/app/assets/javascripts/frequent_items/store/getters.js index 73e66643f06..36cc9020d8d 100644 --- a/app/assets/javascripts/frequent_items/store/getters.js +++ b/app/assets/javascripts/frequent_items/store/getters.js @@ -1,2 +1 @@ -// eslint-disable-next-line import/prefer-default-export export const hasSearchQuery = state => state.searchQuery !== ''; diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 36c586ddfd2..409733c73b9 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import '@gitlab/at.js'; +import '~/lib/utils/jquery_at_who'; import { escape, template } from 'lodash'; import SidebarMediator from '~/sidebar/sidebar_mediator'; import glRegexp from './lib/utils/regexp'; @@ -52,6 +52,7 @@ export const defaultAutocompleteConfig = { milestones: true, labels: true, snippets: true, + vulnerabilities: true, }; class GfmAutoComplete { @@ -71,12 +72,15 @@ class GfmAutoComplete { setupLifecycle() { this.input.each((i, input) => { const $input = $(input); - $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); - $input.on('change.atwho', () => input.dispatchEvent(new Event('input'))); - // This triggers at.js again - // Needed for quick actions with suffixes (ex: /label ~) - $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); - $input.on('clear-commands-cache.atwho', () => this.clearCache()); + if (!$input.hasClass('js-gfm-input-initialized')) { + $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); + $input.on('change.atwho', () => input.dispatchEvent(new Event('input'))); + // This triggers at.js again + // Needed for quick actions with suffixes (ex: /label ~) + $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); + $input.on('clear-commands-cache.atwho', () => this.clearCache()); + $input.addClass('js-gfm-input-initialized'); + } }); } @@ -644,7 +648,8 @@ class GfmAutoComplete { // https://github.com/ichord/At.js const atSymbolsWithBar = Object.keys(controllers) .join('|') - .replace(/[$]/, '\\$&'); + .replace(/[$]/, '\\$&') + .replace(/[+]/, '\\+'); const atSymbolsWithoutBar = Object.keys(controllers).join(''); const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop(); const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); @@ -675,6 +680,7 @@ GfmAutoComplete.atTypeMap = { '~': 'labels', '%': 'milestones', '/': 'commands', + '+': 'vulnerabilities', $: 'snippets', }; diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js index 099c46f4b8d..e0f64c8e843 100644 --- a/app/assets/javascripts/gpg_badges.js +++ b/app/assets/javascripts/gpg_badges.js @@ -13,7 +13,8 @@ export default class GpgBadges { const badges = $('.js-loading-gpg-badge'); - badges.html('<i class="fa fa-spinner fa-spin"></i>'); + badges.html('<span class="gl-spinner gl-spinner-orange gl-spinner-sm"></span>'); + badges.children().attr('aria-label', __('Loading')); const displayError = () => createFlash(__('An error occurred while loading commit signatures')); diff --git a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue index 59327e36f5f..79494cb173b 100644 --- a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue +++ b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue @@ -1,7 +1,6 @@ <script> -import { GlButton, GlFormGroup, GlFormInput, GlFormCheckbox } from '@gitlab/ui'; +import { GlButton, GlFormGroup, GlFormInput, GlFormCheckbox, GlIcon } from '@gitlab/ui'; import { mapState, mapActions } from 'vuex'; -import Icon from '~/vue_shared/components/icon.vue'; export default { components: { @@ -9,7 +8,7 @@ export default { GlFormCheckbox, GlFormGroup, GlFormInput, - Icon, + GlIcon, }, data() { return { placeholderUrl: 'https://my-url.grafana.net/' }; @@ -89,7 +88,7 @@ export default { rel="noopener noreferrer" > {{ __('More information') }} - <icon name="external-link" class="vertical-align-middle" /> + <gl-icon name="external-link" class="vertical-align-middle" /> </a> </p> </gl-form-group> diff --git a/app/assets/javascripts/graphql_shared/fragments/author.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/author.fragment.graphql index 9a2ff1c1648..0855ac2af45 100644 --- a/app/assets/javascripts/graphql_shared/fragments/author.fragment.graphql +++ b/app/assets/javascripts/graphql_shared/fragments/author.fragment.graphql @@ -1,4 +1,5 @@ fragment Author on User { + id avatarUrl name username diff --git a/app/assets/javascripts/graphql_shared/fragments/epic.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/epic.fragment.graphql new file mode 100644 index 00000000000..286ebbd019e --- /dev/null +++ b/app/assets/javascripts/graphql_shared/fragments/epic.fragment.graphql @@ -0,0 +1,10 @@ +fragment EpicNode on Epic { + id + iid + title + state + reference + webUrl + createdAt + closedAt +} diff --git a/app/assets/javascripts/graphql_shared/fragments/label.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/label.fragment.graphql new file mode 100644 index 00000000000..1a2ea0bda1b --- /dev/null +++ b/app/assets/javascripts/graphql_shared/fragments/label.fragment.graphql @@ -0,0 +1,7 @@ +fragment Label on Label { + id + title + description + color + textColor +} diff --git a/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_mark_done.mutation.graphql b/app/assets/javascripts/graphql_shared/mutations/todo_mark_done.mutation.graphql index 4d59b4d94cd..4d59b4d94cd 100644 --- a/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_mark_done.mutation.graphql +++ b/app/assets/javascripts/graphql_shared/mutations/todo_mark_done.mutation.graphql diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue index 0b401f4d732..871f5c9a845 100644 --- a/app/assets/javascripts/groups/components/app.vue +++ b/app/assets/javascripts/groups/components/app.vue @@ -2,6 +2,7 @@ /* global Flash */ import $ from 'jquery'; +import 'vendor/jquery.scrollTo'; import { GlLoadingIcon } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index be90ba12678..44349b33386 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { GlLoadingIcon, GlBadge } from '@gitlab/ui'; import { visitUrl } from '../../lib/utils/url_utility'; import tooltip from '../../vue_shared/directives/tooltip'; diff --git a/app/assets/javascripts/groups/components/invite_members_banner.vue b/app/assets/javascripts/groups/components/invite_members_banner.vue new file mode 100644 index 00000000000..da7adab1d86 --- /dev/null +++ b/app/assets/javascripts/groups/components/invite_members_banner.vue @@ -0,0 +1,76 @@ +<script> +import { GlBanner } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { parseBoolean, setCookie, getCookie } from '~/lib/utils/common_utils'; +import Tracking from '~/tracking'; + +const trackingMixin = Tracking.mixin(); + +export default { + components: { + GlBanner, + }, + mixins: [trackingMixin], + inject: ['svgPath', 'inviteMembersPath', 'isDismissedKey', 'trackLabel'], + data() { + return { + isDismissed: parseBoolean(getCookie(this.isDismissedKey)), + tracking: { + label: this.trackLabel, + }, + }; + }, + created() { + this.$nextTick(() => { + this.addTrackingAttributesToButton(); + }); + }, + mounted() { + this.trackOnShow(); + }, + methods: { + handleClose() { + setCookie(this.isDismissedKey, true); + this.isDismissed = true; + this.track(this.$options.dismissEvent); + }, + trackOnShow() { + if (!this.isDismissed) this.track(this.$options.displayEvent); + }, + addTrackingAttributesToButton() { + if (this.$refs.banner === undefined) return; + + const button = this.$refs.banner.$el.querySelector(`[href='${this.inviteMembersPath}']`); + + if (button) { + button.setAttribute('data-track-event', this.$options.buttonClickEvent); + button.setAttribute('data-track-label', this.trackLabel); + } + }, + }, + i18n: { + title: s__('InviteMembersBanner|Collaborate with your team'), + body: s__( + "InviteMembersBanner|We noticed that you haven't invited anyone to this group. Invite your colleagues so you can discuss issues, collaborate on merge requests, and share your knowledge.", + ), + button_text: s__('InviteMembersBanner|Invite your colleagues'), + }, + displayEvent: 'invite_members_banner_displayed', + buttonClickEvent: 'invite_members_banner_button_clicked', + dismissEvent: 'invite_members_banner_dismissed', +}; +</script> + +<template> + <gl-banner + v-if="!isDismissed" + ref="banner" + :title="$options.i18n.title" + :button-text="$options.i18n.button_text" + :svg-path="svgPath" + :button-link="inviteMembersPath" + @close="handleClose" + > + <p>{{ $options.i18n.body }}</p> + </gl-banner> +</template> diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue index ac4c12dda24..5487e25066e 100644 --- a/app/assets/javascripts/groups/components/item_actions.vue +++ b/app/assets/javascripts/groups/components/item_actions.vue @@ -1,12 +1,12 @@ <script> +import { GlIcon } from '@gitlab/ui'; import tooltip from '~/vue_shared/directives/tooltip'; -import icon from '~/vue_shared/components/icon.vue'; import eventHub from '../event_hub'; import { COMMON_STR } from '../constants'; export default { components: { - icon, + GlIcon, }, directives: { tooltip, @@ -56,7 +56,7 @@ export default { class="leave-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5" @click.prevent="onLeaveGroup" > - <icon name="leave" class="position-top-0" /> + <gl-icon name="leave" class="position-top-0" /> </a> <a v-if="group.canEdit" @@ -68,7 +68,7 @@ export default { data-placement="bottom" class="edit-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5" > - <icon name="settings" class="position-top-0 align-middle" /> + <gl-icon name="settings" class="position-top-0 align-middle" /> </a> </div> </template> diff --git a/app/assets/javascripts/groups/components/item_caret.vue b/app/assets/javascripts/groups/components/item_caret.vue index cd3e3de4cb4..e23b0fa7413 100644 --- a/app/assets/javascripts/groups/components/item_caret.vue +++ b/app/assets/javascripts/groups/components/item_caret.vue @@ -1,9 +1,9 @@ <script> -import icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; export default { components: { - icon, + GlIcon, }, props: { isGroupOpen: { @@ -21,5 +21,5 @@ export default { </script> <template> - <span class="folder-caret gl-mr-2"> <icon :size="10" :name="iconClass" /> </span> + <span class="folder-caret gl-mr-2"> <gl-icon :size="10" :name="iconClass" /> </span> </template> diff --git a/app/assets/javascripts/groups/components/item_stats_value.vue b/app/assets/javascripts/groups/components/item_stats_value.vue index 27b1c632643..18efd8c6823 100644 --- a/app/assets/javascripts/groups/components/item_stats_value.vue +++ b/app/assets/javascripts/groups/components/item_stats_value.vue @@ -1,10 +1,10 @@ <script> +import { GlIcon } from '@gitlab/ui'; import tooltip from '~/vue_shared/directives/tooltip'; -import icon from '~/vue_shared/components/icon.vue'; export default { components: { - icon, + GlIcon, }, directives: { tooltip, @@ -57,6 +57,6 @@ export default { :title="title" data-container="body" > - <icon :name="iconName" /> <span v-if="isValuePresent" class="stat-value"> {{ value }} </span> + <gl-icon :name="iconName" /> <span v-if="isValuePresent" class="stat-value"> {{ value }} </span> </span> </template> diff --git a/app/assets/javascripts/groups/components/item_type_icon.vue b/app/assets/javascripts/groups/components/item_type_icon.vue index ae69fbd7bde..c3787c2df21 100644 --- a/app/assets/javascripts/groups/components/item_type_icon.vue +++ b/app/assets/javascripts/groups/components/item_type_icon.vue @@ -1,10 +1,10 @@ <script> -import icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import { ITEM_TYPE } from '../constants'; export default { components: { - icon, + GlIcon, }, props: { itemType: { @@ -29,5 +29,5 @@ export default { </script> <template> - <span class="item-type-icon"> <icon :name="iconClass" /> </span> + <span class="item-type-icon"> <gl-icon :name="iconClass" /> </span> </template> diff --git a/app/assets/javascripts/groups/init_invite_members_banner.js b/app/assets/javascripts/groups/init_invite_members_banner.js new file mode 100644 index 00000000000..c7967827917 --- /dev/null +++ b/app/assets/javascripts/groups/init_invite_members_banner.js @@ -0,0 +1,23 @@ +import Vue from 'vue'; +import InviteMembersBanner from '~/groups/components/invite_members_banner.vue'; + +export default function initInviteMembersBanner() { + const el = document.querySelector('.js-group-invite-members-banner'); + + if (!el) { + return false; + } + + const { svgPath, inviteMembersPath, isDismissedKey, trackLabel } = el.dataset; + + return new Vue({ + el, + provide: { + svgPath, + inviteMembersPath, + isDismissedKey, + trackLabel, + }, + render: createElement => createElement(InviteMembersBanner), + }); +} diff --git a/app/assets/javascripts/groups/members/components/app.vue b/app/assets/javascripts/groups/members/components/app.vue new file mode 100644 index 00000000000..e94b28f5773 --- /dev/null +++ b/app/assets/javascripts/groups/members/components/app.vue @@ -0,0 +1,11 @@ +<script> +export default { + name: 'GroupMembersApp', +}; +</script> + +<template> + <span> + <!-- Temporary empty template --> + </span> +</template> diff --git a/app/assets/javascripts/groups/members/index.js b/app/assets/javascripts/groups/members/index.js new file mode 100644 index 00000000000..4ca1756f10c --- /dev/null +++ b/app/assets/javascripts/groups/members/index.js @@ -0,0 +1,30 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import App from './components/app.vue'; +import membersModule from '~/vuex_shared/modules/members'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; + +export default el => { + if (!el) { + return () => {}; + } + + Vue.use(Vuex); + + const { members, groupId } = el.dataset; + + const store = new Vuex.Store({ + ...membersModule({ + members: convertObjectPropsToCamelCase(JSON.parse(members), { deep: true }), + sourceId: parseInt(groupId, 10), + currentUserId: gon.current_user_id || null, + }), + }); + + return new Vue({ + el, + components: { App }, + store, + render: createElement => createElement('app'), + }); +}; diff --git a/app/assets/javascripts/groups/transfer_dropdown.js b/app/assets/javascripts/groups/transfer_dropdown.js index e94b163dfb1..cefd803c631 100644 --- a/app/assets/javascripts/groups/transfer_dropdown.js +++ b/app/assets/javascripts/groups/transfer_dropdown.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import { __ } from '~/locale'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default class TransferDropdown { constructor() { @@ -16,7 +17,7 @@ export default class TransferDropdown { buildDropdown() { const extraOptions = [{ id: '-1', text: __('No parent group') }, { type: 'divider' }]; - this.groupDropdown.glDropdown({ + initDeprecatedJQueryDropdown(this.groupDropdown, { selectable: true, filterable: true, toggleLabel: item => item.text, diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 4c50bb3a9ac..aac23db8fd6 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -97,7 +97,10 @@ const groupsSelect = () => { }); }; -export default () => - import(/* webpackChunkName: 'select2' */ 'select2/select2') - .then(groupsSelect) - .catch(() => {}); +export default () => { + if ($('.ajax-groups-select').length) { + import(/* webpackChunkName: 'select2' */ 'select2/select2') + .then(groupsSelect) + .catch(() => {}); + } +}; diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index 3f9163e924d..b833cca1db6 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -2,9 +2,6 @@ import $ from 'jquery'; import Vue from 'vue'; import Translate from '~/vue_shared/translate'; import { highCountTrim } from '~/lib/utils/text_utility'; -import SetStatusModalTrigger from './set_status_modal/set_status_modal_trigger.vue'; -import SetStatusModalWrapper from './set_status_modal/set_status_modal_wrapper.vue'; -import { parseBoolean } from '~/lib/utils/common_utils'; import Tracking from '~/tracking'; /** @@ -17,7 +14,7 @@ import Tracking from '~/tracking'; export default function initTodoToggle() { $(document).on('todo:toggle', (e, count) => { const updatedCount = count || e?.detail?.count || 0; - const $todoPendingCount = $('.todos-count'); + const $todoPendingCount = $('.js-todos-count'); $todoPendingCount.text(highCountTrim(updatedCount)); $todoPendingCount.toggleClass('hidden', updatedCount === 0); @@ -26,51 +23,43 @@ export default function initTodoToggle() { function initStatusTriggers() { const setStatusModalTriggerEl = document.querySelector('.js-set-status-modal-trigger'); - const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper'); - if (setStatusModalTriggerEl || setStatusModalWrapperEl) { - Vue.use(Translate); + if (setStatusModalTriggerEl) { + setStatusModalTriggerEl.addEventListener('click', () => { + import( + /* webpackChunkName: 'statusModalBundle' */ './set_status_modal/set_status_modal_wrapper.vue' + ) + .then(({ default: SetStatusModalWrapper }) => { + const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper'); + const statusModalElement = document.createElement('div'); + setStatusModalWrapperEl.appendChild(statusModalElement); - // eslint-disable-next-line no-new - new Vue({ - el: setStatusModalTriggerEl, - data() { - const { hasStatus } = this.$options.el.dataset; + Vue.use(Translate); - return { - hasStatus: parseBoolean(hasStatus), - }; - }, - render(createElement) { - return createElement(SetStatusModalTrigger, { - props: { - hasStatus: this.hasStatus, - }, - }); - }, - }); - - // eslint-disable-next-line no-new - new Vue({ - el: setStatusModalWrapperEl, - data() { - const { currentEmoji, currentMessage } = this.$options.el.dataset; + // eslint-disable-next-line no-new + new Vue({ + el: statusModalElement, + data() { + const { currentEmoji, currentMessage } = setStatusModalWrapperEl.dataset; - return { - currentEmoji, - currentMessage, - }; - }, - render(createElement) { - const { currentEmoji, currentMessage } = this; + return { + currentEmoji, + currentMessage, + }; + }, + render(createElement) { + const { currentEmoji, currentMessage } = this; - return createElement(SetStatusModalWrapper, { - props: { - currentEmoji, - currentMessage, - }, - }); - }, + return createElement(SetStatusModalWrapper, { + props: { + currentEmoji, + currentMessage, + }, + }); + }, + }); + }) + .catch(() => {}); }); } } @@ -101,5 +90,5 @@ export function initNavUserDropdownTracking() { document.addEventListener('DOMContentLoaded', () => { requestIdleCallback(initStatusTriggers); - initNavUserDropdownTracking(); + requestIdleCallback(initNavUserDropdownTracking); }); diff --git a/app/assets/javascripts/helpers/startup_css_helper.js b/app/assets/javascripts/helpers/startup_css_helper.js new file mode 100644 index 00000000000..8e25e1421c0 --- /dev/null +++ b/app/assets/javascripts/helpers/startup_css_helper.js @@ -0,0 +1,46 @@ +const CSS_LOADED_EVENT = 'CSSLoaded'; +const STARTUP_LINK_LOADED_EVENT = 'CSSStartupLinkLoaded'; + +const getAllStartupLinks = (() => { + let links = null; + return () => { + if (!links) { + links = Array.from(document.querySelectorAll('link[data-startupcss]')); + } + return links; + }; +})(); +const isStartupLinkLoaded = ({ dataset }) => dataset.startupcss === 'loaded'; +const allLinksLoaded = () => getAllStartupLinks().every(isStartupLinkLoaded); + +const handleStartupEvents = () => { + if (allLinksLoaded()) { + document.dispatchEvent(new CustomEvent(CSS_LOADED_EVENT)); + document.removeEventListener(STARTUP_LINK_LOADED_EVENT, handleStartupEvents); + } +}; + +/* Wait for.... The methods can be used: + - with a callback (preferred), + waitFor(action) + + - with then (discouraged), + await waitFor().then(action); + + - with await, + await waitFor; + action(); +-*/ +export const waitForCSSLoaded = (action = () => {}) => { + if (!gon.features.startupCss || allLinksLoaded()) { + return new Promise(resolve => { + action(); + 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/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue index a65af55fcac..183816921c1 100644 --- a/app/assets/javascripts/ide/components/activity_bar.vue +++ b/app/assets/javascripts/ide/components/activity_bar.vue @@ -1,13 +1,13 @@ <script> import $ from 'jquery'; import { mapActions, mapState } from 'vuex'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import tooltip from '~/vue_shared/directives/tooltip'; import { leftSidebarViews } from '../constants'; export default { components: { - Icon, + GlIcon, }, directives: { tooltip, @@ -44,11 +44,12 @@ export default { :aria-label="s__('IDE|Edit')" data-container="body" data-placement="right" + data-qa-selector="edit_mode_tab" type="button" class="ide-sidebar-link js-ide-edit-mode" @click.prevent="changedActivityView($event, $options.leftSidebarViews.edit.name)" > - <icon name="code" /> + <gl-icon name="code" /> </button> </li> <li> @@ -65,7 +66,7 @@ export default { class="ide-sidebar-link js-ide-review-mode" @click.prevent="changedActivityView($event, $options.leftSidebarViews.review.name)" > - <icon name="file-modified" /> + <gl-icon name="file-modified" /> </button> </li> <li> @@ -78,11 +79,12 @@ export default { :aria-label="s__('IDE|Commit')" data-container="body" data-placement="right" + data-qa-selector="commit_mode_tab" type="button" - class="ide-sidebar-link js-ide-commit-mode qa-commit-mode-tab" + class="ide-sidebar-link js-ide-commit-mode" @click.prevent="changedActivityView($event, $options.leftSidebarViews.commit.name)" > - <icon name="commit" /> + <gl-icon name="commit" /> </button> </li> </ul> diff --git a/app/assets/javascripts/ide/components/branches/item.vue b/app/assets/javascripts/ide/components/branches/item.vue index 49744d573da..2fe435b92ab 100644 --- a/app/assets/javascripts/ide/components/branches/item.vue +++ b/app/assets/javascripts/ide/components/branches/item.vue @@ -1,11 +1,11 @@ <script> /* eslint-disable @gitlab/vue-require-i18n-strings */ -import Icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import Timeago from '~/vue_shared/components/time_ago_tooltip.vue'; export default { components: { - Icon, + GlIcon, Timeago, }, props: { @@ -34,7 +34,7 @@ export default { <template> <a :href="branchHref" class="btn-link d-flex align-items-center"> <span class="d-flex gl-mr-3 ide-search-list-current-icon"> - <icon v-if="isActive" :size="18" name="mobile-issue-close" /> + <gl-icon v-if="isActive" :size="18" name="mobile-issue-close" /> </span> <span> <strong> {{ item.name }} </strong> diff --git a/app/assets/javascripts/ide/components/branches/search_list.vue b/app/assets/javascripts/ide/components/branches/search_list.vue index dd2d726d525..c317fadb656 100644 --- a/app/assets/javascripts/ide/components/branches/search_list.vue +++ b/app/assets/javascripts/ide/components/branches/search_list.vue @@ -1,14 +1,13 @@ <script> import { mapActions, mapState } from 'vuex'; import { debounce } from 'lodash'; -import { GlLoadingIcon } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; import Item from './item.vue'; export default { components: { Item, - Icon, + GlIcon, GlLoadingIcon, }, data() { @@ -67,7 +66,7 @@ export default { class="form-control dropdown-input-field" @input="searchBranches" /> - <icon :size="18" name="search" class="ml-3 input-icon" /> + <gl-icon :size="18" name="search" class="ml-3 input-icon" /> </label> <div class="dropdown-content ide-merge-requests-dropdown-content d-flex"> <gl-loading-icon diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue index 407e4c57cd8..de4b0a34002 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { escape } from 'lodash'; import { mapState, mapGetters, createNamespacedHelpers } from 'vuex'; import { sprintf, s__ } from '~/locale'; diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue index 9342ab87c1a..73c56514fce 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue @@ -1,6 +1,6 @@ <script> import { mapState, mapActions, mapGetters } from 'vuex'; -import { GlModal } from '@gitlab/ui'; +import { GlModal, GlSafeHtmlDirective } from '@gitlab/ui'; import { n__, __ } from '~/locale'; import LoadingButton from '~/vue_shared/components/loading_button.vue'; import CommitMessageField from './message_field.vue'; @@ -8,6 +8,7 @@ import Actions from './actions.vue'; import SuccessMessage from './success_message.vue'; import { leftSidebarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants'; import consts from '../../stores/modules/commit/constants'; +import { createUnexpectedCommitError } from '../../lib/errors'; export default { components: { @@ -17,15 +18,20 @@ export default { SuccessMessage, GlModal, }, + directives: { + SafeHtml: GlSafeHtmlDirective, + }, data() { return { isCompact: true, componentHeight: null, + // Keep track of "lastCommitError" so we hold onto the value even when "commitError" is cleared. + lastCommitError: createUnexpectedCommitError(), }; }, computed: { ...mapState(['changedFiles', 'stagedFiles', 'currentActivityView', 'lastCommitMsg']), - ...mapState('commit', ['commitMessage', 'submitCommitLoading']), + ...mapState('commit', ['commitMessage', 'submitCommitLoading', 'commitError']), ...mapGetters(['someUncommittedChanges']), ...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']), overviewText() { @@ -38,11 +44,28 @@ export default { currentViewIsCommitView() { return this.currentActivityView === leftSidebarViews.commit.name; }, + commitErrorPrimaryAction() { + if (!this.lastCommitError?.canCreateBranch) { + return undefined; + } + + return { + text: __('Create new branch'), + }; + }, }, watch: { currentActivityView: 'handleCompactState', someUncommittedChanges: 'handleCompactState', lastCommitMsg: 'handleCompactState', + commitError(val) { + if (!val) { + return; + } + + this.lastCommitError = val; + this.$refs.commitErrorModal.show(); + }, }, methods: { ...mapActions(['updateActivityBarView']), @@ -53,9 +76,7 @@ export default { 'updateCommitAction', ]), commit() { - return this.commitChanges().catch(() => { - this.$refs.createBranchModal.show(); - }); + return this.commitChanges(); }, forceCreateNewBranch() { return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commit()); @@ -164,17 +185,14 @@ export default { </button> </div> <gl-modal - ref="createBranchModal" - modal-id="ide-create-branch-modal" - :ok-title="__('Create new branch')" - :title="__('Branch has changed')" - ok-variant="success" + ref="commitErrorModal" + modal-id="ide-commit-error-modal" + :title="lastCommitError.title" + :action-primary="commitErrorPrimaryAction" + :action-cancel="{ text: __('Cancel') }" @ok="forceCreateNewBranch" > - {{ - __(`This branch has changed since you started editing. - Would you like to create a new branch?`) - }} + <div v-safe-html="lastCommitError.messageHTML"></div> </gl-modal> </form> </transition> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue index d1422a506e7..609ce287d3f 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue @@ -1,14 +1,13 @@ <script> import { mapActions } from 'vuex'; -import { GlModal } from '@gitlab/ui'; +import { GlModal, GlIcon } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; import ListItem from './list_item.vue'; export default { components: { - Icon, + GlIcon, ListItem, GlModal, }, @@ -74,7 +73,7 @@ export default { <div class="ide-commit-list-container"> <header class="multi-file-commit-panel-header d-flex mb-0"> <div class="d-flex align-items-center flex-fill"> - <icon v-once :name="iconName" :size="18" class="gl-mr-3" /> + <gl-icon v-once :name="iconName" :size="18" class="gl-mr-3" /> <strong> {{ titleText }} </strong> <div class="d-flex ml-auto"> <button @@ -93,7 +92,7 @@ export default { data-boundary="viewport" @click="openDiscardModal" > - <icon :size="16" name="remove-all" class="ml-auto mr-auto position-top-0" /> + <gl-icon :size="16" name="remove-all" class="ml-auto mr-auto position-top-0" /> </button> </div> </div> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue index 1b257ca11cc..4821b8389ff 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue @@ -1,11 +1,11 @@ <script> -import Icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import tooltip from '~/vue_shared/directives/tooltip'; import { sprintf, n__, __ } from '~/locale'; export default { components: { - Icon, + GlIcon, }, directives: { tooltip, @@ -77,7 +77,7 @@ export default { data-placement="left" class="gl-mb-5" > - <icon v-once :name="iconName" :size="18" /> + <gl-icon v-once :name="iconName" :size="18" /> </div> <div v-tooltip @@ -86,7 +86,7 @@ export default { data-placement="left" class="gl-mb-3" > - <icon :name="additionIconName" :size="18" :class="addedFilesIconClass" /> + <gl-icon :name="additionIconName" :size="18" :class="addedFilesIconClass" /> </div> {{ addedFilesLength }} <div @@ -96,7 +96,7 @@ export default { data-placement="left" class="gl-mt-3 gl-mb-3" > - <icon :name="modifiedIconName" :size="18" :class="modifiedFilesClass" /> + <gl-icon :name="modifiedIconName" :size="18" :class="modifiedFilesClass" /> </div> {{ modifiedFilesLength }} </div> 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 c65169f5d31..a0d6cf3c42d 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue @@ -1,14 +1,14 @@ <script> import { mapActions } from 'vuex'; +import { GlIcon } from '@gitlab/ui'; import tooltip from '~/vue_shared/directives/tooltip'; -import Icon from '~/vue_shared/components/icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import { viewerTypes } from '../../constants'; import getCommitIconMap from '../../commit_icon'; export default { components: { - Icon, + GlIcon, FileIcon, }, directives: { @@ -95,7 +95,7 @@ export default { </span> <div class="ml-auto d-flex align-items-center"> <div class="d-flex align-items-center ide-commit-list-changed-icon"> - <icon :name="iconName" :size="16" :class="iconClass" /> + <gl-icon :name="iconName" :size="16" :class="iconClass" /> </div> </div> </div> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue index b37c7280a30..2787b10a48b 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue @@ -1,6 +1,6 @@ <script> +import { GlIcon } from '@gitlab/ui'; import { __, sprintf } from '../../../locale'; -import Icon from '../../../vue_shared/components/icon.vue'; import popover from '../../../vue_shared/directives/popover'; import { MAX_TITLE_LENGTH, MAX_BODY_LENGTH } from '../../constants'; @@ -9,7 +9,7 @@ export default { popover, }, components: { - Icon, + GlIcon, }, props: { text: { @@ -84,7 +84,7 @@ export default { <li> {{ __('Commit Message') }} <span v-popover="$options.popoverOptions" class="form-text text-muted gl-ml-3"> - <icon name="question" /> + <gl-icon name="question" /> </span> </li> </ul> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue index 327b0b8172f..977efb0ca22 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { mapState } from 'vuex'; export default { diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue index d36adbd798e..08635b43b91 100644 --- a/app/assets/javascripts/ide/components/error_message.vue +++ b/app/assets/javascripts/ide/components/error_message.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { mapActions } from 'vuex'; import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue index f7cf7a5b251..48ab58e1cb7 100644 --- a/app/assets/javascripts/ide/components/file_row_extra.vue +++ b/app/assets/javascripts/ide/components/file_row_extra.vue @@ -1,8 +1,8 @@ <script> import { mapGetters } from 'vuex'; +import { GlIcon } from '@gitlab/ui'; import { n__ } from '~/locale'; import tooltip from '~/vue_shared/directives/tooltip'; -import Icon from '~/vue_shared/components/icon.vue'; import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; import NewDropdown from './new_dropdown/index.vue'; import MrFileIcon from './mr_file_icon.vue'; @@ -13,7 +13,7 @@ export default { tooltip, }, components: { - Icon, + GlIcon, NewDropdown, ChangedFileIcon, MrFileIcon, @@ -69,7 +69,7 @@ export default { <mr-file-icon v-if="file.mrChange" /> <span v-if="showTreeChangesCount" class="ide-tree-changes"> {{ changesCount }} - <icon + <gl-icon v-tooltip :title="folderChangesTooltip" :size="12" diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 55b3eaf9737..1b03d9eee8b 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -47,6 +47,7 @@ export default { 'emptyRepo', 'currentTree', 'editorTheme', + 'getUrlForPath', ]), themeName() { return window.gon?.user_color_scheme; @@ -71,7 +72,7 @@ export default { return returnValue; }, openFile(file) { - this.$router.push(`/project${file.url}`); + this.$router.push(this.getUrlForPath(file.path)); }, createNewFile() { this.$refs.newModal.open(modalTypes.blob); diff --git a/app/assets/javascripts/ide/components/ide_file_row.vue b/app/assets/javascripts/ide/components/ide_file_row.vue index b777d89f0bb..248677d6a99 100644 --- a/app/assets/javascripts/ide/components/ide_file_row.vue +++ b/app/assets/javascripts/ide/components/ide_file_row.vue @@ -3,6 +3,7 @@ * This component is an iterative step towards refactoring and simplifying `vue_shared/components/file_row.vue` * https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23720 */ +import { mapGetters } from 'vuex'; import FileRow from '~/vue_shared/components/file_row.vue'; import FileRowExtra from './file_row_extra.vue'; @@ -23,6 +24,9 @@ export default { dropdownOpen: false, }; }, + computed: { + ...mapGetters(['getUrlForPath']), + }, methods: { toggleDropdown(val) { this.dropdownOpen = val; @@ -32,7 +36,13 @@ export default { </script> <template> - <file-row :file="file" v-bind="$attrs" @mouseleave="toggleDropdown(false)" v-on="$listeners"> + <file-row + :file="file" + :file-url="getUrlForPath(file.path)" + v-bind="$attrs" + @mouseleave="toggleDropdown(false)" + v-on="$listeners" + > <file-row-extra :file="file" :dropdown-open="dropdownOpen" @toggle="toggleDropdown($event)" /> </file-row> </template> diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue index 95348711e1d..e36d0a5a5b1 100644 --- a/app/assets/javascripts/ide/components/ide_review.vue +++ b/app/assets/javascripts/ide/components/ide_review.vue @@ -10,7 +10,7 @@ export default { EditorModeDropdown, }, computed: { - ...mapGetters(['currentMergeRequest', 'activeFile']), + ...mapGetters(['currentMergeRequest', 'activeFile', 'getUrlForPath']), ...mapState(['viewer', 'currentMergeRequestId']), showLatestChangesText() { return !this.currentMergeRequestId || this.viewer === viewerTypes.diff; @@ -24,7 +24,7 @@ export default { }, mounted() { if (this.activeFile && this.activeFile.pending && !this.activeFile.deleted) { - this.$router.push(`/project${this.activeFile.url}`, () => { + this.$router.push(this.getUrlForPath(this.activeFile.path), () => { this.updateViewer('editor'); }); } else if (this.activeFile && this.activeFile.deleted) { diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index 1eb89b41495..ed68ca5cae9 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -1,6 +1,6 @@ <script> import { mapState, mapGetters } from 'vuex'; -import { GlSkeletonLoading } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import IdeTree from './ide_tree.vue'; import ResizablePanel from './resizable_panel.vue'; import ActivityBar from './activity_bar.vue'; diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index ddc126c3d77..146e818d654 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -1,9 +1,9 @@ <script> /* eslint-disable @gitlab/vue-require-i18n-strings */ import { mapActions, mapState, mapGetters } from 'vuex'; +import { GlIcon } from '@gitlab/ui'; import IdeStatusList from './ide_status_list.vue'; import IdeStatusMr from './ide_status_mr.vue'; -import icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; import timeAgoMixin from '~/vue_shared/mixins/timeago'; import CiIcon from '../../vue_shared/components/ci_icon.vue'; @@ -12,7 +12,7 @@ import { rightSidebarViews } from '../constants'; export default { components: { - icon, + GlIcon, userAvatarImage, CiIcon, IdeStatusList, @@ -97,12 +97,13 @@ export default { {{ latestPipeline.details.status.text }} for </span> - <icon name="commit" /> + <gl-icon name="commit" /> <a v-tooltip :title="lastCommit.message" :href="getCommitPath(lastCommit.short_id)" class="commit-sha" + data-qa-selector="commit_sha_content" >{{ lastCommit.short_id }}</a > by diff --git a/app/assets/javascripts/ide/components/ide_status_list.vue b/app/assets/javascripts/ide/components/ide_status_list.vue index 1354fdc3d98..caa122f6ed2 100644 --- a/app/assets/javascripts/ide/components/ide_status_list.vue +++ b/app/assets/javascripts/ide/components/ide_status_list.vue @@ -2,7 +2,7 @@ import { mapGetters } from 'vuex'; import { GlLink, GlTooltipDirective } from '@gitlab/ui'; import TerminalSyncStatusSafe from './terminal_sync/terminal_sync_status_safe.vue'; -import { getFileEOL } from '../utils'; +import { isTextFile, getFileEOL } from '~/ide/utils'; export default { components: { @@ -17,6 +17,9 @@ export default { activeFileEOL() { return getFileEOL(this.activeFile.content); }, + activeFileIsText() { + return isTextFile(this.activeFile); + }, }, }; </script> @@ -30,7 +33,7 @@ export default { </gl-link> </div> <div>{{ activeFileEOL }}</div> - <div v-if="!activeFile.binary">{{ activeFile.editorRow }}:{{ activeFile.editorColumn }}</div> + <div v-if="activeFileIsText">{{ activeFile.editorRow }}:{{ activeFile.editorColumn }}</div> <div>{{ activeFile.fileLanguage }}</div> </template> <terminal-sync-status-safe /> diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue index 647f4d4be85..747d5044790 100644 --- a/app/assets/javascripts/ide/components/ide_tree.vue +++ b/app/assets/javascripts/ide/components/ide_tree.vue @@ -15,13 +15,13 @@ export default { }, computed: { ...mapState(['currentBranchId']), - ...mapGetters(['currentProject', 'currentTree', 'activeFile']), + ...mapGetters(['currentProject', 'currentTree', 'activeFile', 'getUrlForPath']), }, mounted() { if (!this.activeFile) return; if (this.activeFile.pending && !this.activeFile.deleted) { - this.$router.push(`/project${this.activeFile.url}`, () => { + this.$router.push(this.getUrlForPath(this.activeFile.path), () => { this.updateViewer('editor'); }); } else if (this.activeFile.deleted) { diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index 36e8951bea3..776d8459515 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -1,6 +1,6 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; -import { GlSkeletonLoading } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import FileTree from '~/vue_shared/components/file_tree.vue'; import IdeFileRow from './ide_file_row.vue'; import NavDropdown from './nav_dropdown.vue'; diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue index 975d54c7a4e..11033a5cc88 100644 --- a/app/assets/javascripts/ide/components/jobs/detail.vue +++ b/app/assets/javascripts/ide/components/jobs/detail.vue @@ -1,9 +1,10 @@ <script> +/* eslint-disable vue/no-v-html */ import { mapActions, mapState } from 'vuex'; import { throttle } from 'lodash'; +import { GlIcon } from '@gitlab/ui'; import { __ } from '../../../locale'; import tooltip from '../../../vue_shared/directives/tooltip'; -import Icon from '../../../vue_shared/components/icon.vue'; import ScrollButton from './detail/scroll_button.vue'; import JobDescription from './detail/description.vue'; @@ -17,7 +18,7 @@ export default { tooltip, }, components: { - Icon, + GlIcon, ScrollButton, JobDescription, }, @@ -39,10 +40,10 @@ export default { }, }, mounted() { - this.getTrace(); + this.getLogs(); }, methods: { - ...mapActions('pipelines', ['fetchJobTrace', 'setDetailJob']), + ...mapActions('pipelines', ['fetchJobLogs', 'setDetailJob']), scrollDown() { if (this.$refs.buildTrace) { this.$refs.buildTrace.scrollTo(0, this.$refs.buildTrace.scrollHeight); @@ -65,8 +66,8 @@ export default { this.scrollPos = ''; } }), - getTrace() { - return this.fetchJobTrace().then(() => this.scrollDown()); + getLogs() { + return this.fetchJobLogs().then(() => this.scrollDown()); }, }, }; @@ -76,7 +77,7 @@ export default { <div class="ide-pipeline build-page d-flex flex-column flex-fill"> <header class="ide-job-header d-flex align-items-center"> <button class="btn btn-default btn-sm d-flex" @click="setDetailJob(null)"> - <icon name="chevron-left" /> {{ __('View jobs') }} + <gl-icon name="chevron-left" /> {{ __('View jobs') }} </button> </header> <div class="top-bar d-flex border-left-0 mr-3"> diff --git a/app/assets/javascripts/ide/components/jobs/detail/description.vue b/app/assets/javascripts/ide/components/jobs/detail/description.vue index f1ba102fffe..9eaeabad5ef 100644 --- a/app/assets/javascripts/ide/components/jobs/detail/description.vue +++ b/app/assets/javascripts/ide/components/jobs/detail/description.vue @@ -1,10 +1,10 @@ <script> -import Icon from '../../../../vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import CiIcon from '../../../../vue_shared/components/ci_icon.vue'; export default { components: { - Icon, + GlIcon, CiIcon, }, props: { @@ -26,8 +26,14 @@ export default { <ci-icon :status="job.status" :borderless="true" :size="24" class="d-flex" /> <span class="gl-ml-3"> {{ job.name }} - <a :href="job.path" target="_blank" class="ide-external-link position-relative"> - {{ jobId }} <icon :size="12" name="external-link" /> + <a + v-if="job.path" + :href="job.path" + target="_blank" + class="ide-external-link gl-relative" + data-testid="description-detail-link" + > + {{ jobId }} <gl-icon :size="12" name="external-link" /> </a> </span> </div> diff --git a/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue index 5674d3ffa80..2c679a3edc7 100644 --- a/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue +++ b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue @@ -1,6 +1,6 @@ <script> +import { GlIcon } from '@gitlab/ui'; import { __ } from '../../../../locale'; -import Icon from '../../../../vue_shared/components/icon.vue'; import tooltip from '../../../../vue_shared/directives/tooltip'; const directions = { @@ -13,7 +13,7 @@ export default { tooltip, }, components: { - Icon, + GlIcon, }, props: { direction: { @@ -58,7 +58,7 @@ export default { type="button" @click="clickedScroll" > - <icon :name="iconName" /> + <gl-icon :name="iconName" /> </button> </div> </template> diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue index 75441e8c1c8..0b643947139 100644 --- a/app/assets/javascripts/ide/components/jobs/stage.vue +++ b/app/assets/javascripts/ide/components/jobs/stage.vue @@ -1,7 +1,6 @@ <script> -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; import tooltip from '../../../vue_shared/directives/tooltip'; -import Icon from '../../../vue_shared/components/icon.vue'; import CiIcon from '../../../vue_shared/components/ci_icon.vue'; import Item from './item.vue'; @@ -10,7 +9,7 @@ export default { tooltip, }, components: { - Icon, + GlIcon, CiIcon, Item, GlLoadingIcon, @@ -78,7 +77,7 @@ export default { <div v-if="!stage.isLoading || stage.jobs.length" class="gl-mr-3 gl-ml-2"> <span class="badge badge-pill"> {{ jobsCount }} </span> </div> - <icon :name="collapseIcon" class="ide-stage-collapse-icon" /> + <gl-icon :name="collapseIcon" class="ide-stage-collapse-icon" /> </div> <div v-show="!stage.isCollapsed" ref="jobList" class="card-body p-0"> <gl-loading-icon v-if="showLoadingIcon" /> diff --git a/app/assets/javascripts/ide/components/merge_requests/item.vue b/app/assets/javascripts/ide/components/merge_requests/item.vue index 8b7b8d5a91c..7aa9a4f864a 100644 --- a/app/assets/javascripts/ide/components/merge_requests/item.vue +++ b/app/assets/javascripts/ide/components/merge_requests/item.vue @@ -1,9 +1,9 @@ <script> -import Icon from '../../../vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; export default { components: { - Icon, + GlIcon, }, props: { item: { @@ -41,7 +41,7 @@ export default { <template> <a :href="mergeRequestHref" class="btn-link d-flex align-items-center"> <span class="d-flex gl-mr-3 ide-search-list-current-icon"> - <icon v-if="isActive" :size="18" name="mobile-issue-close" /> + <gl-icon v-if="isActive" :size="18" name="mobile-issue-close" /> </span> <span> <strong> {{ item.title }} </strong> diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue index af45d88b84a..4b3c6e61e11 100644 --- a/app/assets/javascripts/ide/components/merge_requests/list.vue +++ b/app/assets/javascripts/ide/components/merge_requests/list.vue @@ -1,9 +1,8 @@ <script> import { mapActions, mapState } from 'vuex'; import { debounce } from 'lodash'; -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; import Item from './item.vue'; import TokenedInput from '../shared/tokened_input.vue'; @@ -16,7 +15,7 @@ export default { components: { TokenedInput, Item, - Icon, + GlIcon, GlLoadingIcon, }, data() { @@ -85,7 +84,7 @@ export default { @input="searchMergeRequests" @removeToken="setSearchType(null)" /> - <icon :size="18" name="search" class="ml-3 input-icon" /> + <gl-icon :size="18" name="search" class="ml-3 input-icon" /> </label> <div class="dropdown-content ide-merge-requests-dropdown-content d-flex"> <gl-loading-icon @@ -103,7 +102,7 @@ export default { @click.stop="setSearchType(searchType)" > <span class="d-flex gl-mr-3 ide-search-list-current-icon"> - <icon :size="18" name="search" /> + <gl-icon :size="18" name="search" /> </span> <span>{{ searchType.label }}</span> </button> diff --git a/app/assets/javascripts/ide/components/mr_file_icon.vue b/app/assets/javascripts/ide/components/mr_file_icon.vue index 4fab57b6f3e..c8629a869e0 100644 --- a/app/assets/javascripts/ide/components/mr_file_icon.vue +++ b/app/assets/javascripts/ide/components/mr_file_icon.vue @@ -1,10 +1,10 @@ <script> -import icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import tooltip from '~/vue_shared/directives/tooltip'; export default { components: { - icon, + GlIcon, }, directives: { tooltip, @@ -13,7 +13,7 @@ export default { </script> <template> - <icon + <gl-icon v-tooltip :title="__('Part of merge request changes')" :size="12" diff --git a/app/assets/javascripts/ide/components/nav_dropdown_button.vue b/app/assets/javascripts/ide/components/nav_dropdown_button.vue index 8dc22620eca..116d3cec03e 100644 --- a/app/assets/javascripts/ide/components/nav_dropdown_button.vue +++ b/app/assets/javascripts/ide/components/nav_dropdown_button.vue @@ -1,13 +1,13 @@ <script> import { mapState } from 'vuex'; +import { GlIcon } from '@gitlab/ui'; import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue'; -import Icon from '~/vue_shared/components/icon.vue'; const EMPTY_LABEL = '-'; export default { components: { - Icon, + GlIcon, DropdownButton, }, props: { @@ -33,10 +33,10 @@ export default { <dropdown-button> <span class="row flex-nowrap"> <span class="col-auto flex-fill text-truncate"> - <icon :size="16" :aria-label="__('Current Branch')" name="branch" /> {{ branchLabel }} + <gl-icon :size="16" :aria-label="__('Current Branch')" name="branch" /> {{ branchLabel }} </span> <span v-if="showMergeRequests" class="col-5 pl-0 text-truncate"> - <icon :size="16" :aria-label="__('Merge Request')" name="merge-request" /> + <gl-icon :size="16" :aria-label="__('Merge Request')" name="merge-request" /> {{ mergeRequestLabel }} </span> </span> diff --git a/app/assets/javascripts/ide/components/new_dropdown/button.vue b/app/assets/javascripts/ide/components/new_dropdown/button.vue index 5bd6642930c..8ae8f97f237 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/button.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/button.vue @@ -1,5 +1,5 @@ <script> -import Icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import tooltip from '~/vue_shared/directives/tooltip'; export default { @@ -7,7 +7,7 @@ export default { tooltip, }, components: { - Icon, + GlIcon, }, props: { label: { @@ -52,7 +52,7 @@ export default { class="btn-blank" @click.stop.prevent="clicked" > - <icon :name="icon" :class="iconClasses" /> + <gl-icon :name="icon" :class="iconClasses" /> <template v-if="showLabel"> {{ label }} </template> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index b656e35f150..692878de5e1 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -1,6 +1,6 @@ <script> import { mapActions } from 'vuex'; -import icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import upload from './upload.vue'; import ItemButton from './button.vue'; import { modalTypes } from '../../constants'; @@ -8,7 +8,7 @@ import NewModal from './modal.vue'; export default { components: { - icon, + GlIcon, upload, ItemButton, NewModal, @@ -67,7 +67,7 @@ export default { data-qa-selector="dropdown_button" @click.stop="openDropdown()" > - <icon name="ellipsis_v" /> <icon name="chevron-down" /> + <gl-icon name="ellipsis_v" /> <gl-icon name="chevron-down" /> </button> <ul ref="dropdownMenu" class="dropdown-menu dropdown-menu-right"> <template v-if="type === 'tree'"> diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index 44986c8c575..528475849de 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -1,6 +1,6 @@ <script> import { mapActions, mapState, mapGetters } from 'vuex'; -import { GlModal } from '@gitlab/ui'; +import { GlModal, GlButton } from '@gitlab/ui'; import { deprecatedCreateFlash as flash } from '~/flash'; import { __, sprintf, s__ } from '~/locale'; import { modalTypes } from '../../constants'; @@ -9,6 +9,7 @@ import { trimPathComponents, getPathParent } from '../../utils'; export default { components: { GlModal, + GlButton, }, data() { return { @@ -156,13 +157,14 @@ export default { /> <ul v-if="isCreatingNewFile" class="file-templates gl-mt-3 list-inline qa-template-list"> <li v-for="(template, index) in templateTypes" :key="index" class="list-inline-item"> - <button - type="button" - class="btn btn-missing p-1 pr-2 pl-2" + <gl-button + variant="dashed" + category="secondary" + class="p-1 pr-2 pl-2" @click="createFromTemplate(template)" > {{ template.name }} - </button> + </gl-button> </li> </ul> </div> diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue index b2141c13d9f..84ff05c9750 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue @@ -28,14 +28,13 @@ export default { const { name } = file; const encodedContent = target.result.split('base64,')[1]; const rawContent = encodedContent ? atob(encodedContent) : ''; - const isText = isTextFile(rawContent, file.type, name); + const isText = isTextFile({ content: rawContent, mimeType: file.type, name }); const emitCreateEvent = content => this.$emit('create', { name: `${this.path ? `${this.path}/` : ''}${name}`, type: 'blob', content, - binary: !isText, rawPath: !isText ? target.result : '', }); diff --git a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue index 4e8e1e3a470..f1b882d8f29 100644 --- a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue +++ b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue @@ -1,7 +1,6 @@ <script> import { mapActions, mapState } from 'vuex'; import tooltip from '~/vue_shared/directives/tooltip'; -import Icon from '~/vue_shared/components/icon.vue'; import IdeSidebarNav from '../ide_sidebar_nav.vue'; export default { @@ -10,7 +9,6 @@ export default { tooltip, }, components: { - Icon, IdeSidebarNav, }, props: { diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue index 6038e92f254..91bd64a2c9c 100644 --- a/app/assets/javascripts/ide/components/pipelines/list.vue +++ b/app/assets/javascripts/ide/components/pipelines/list.vue @@ -1,9 +1,8 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; import { escape } from 'lodash'; -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import { sprintf, __ } from '../../../locale'; -import Icon from '../../../vue_shared/components/icon.vue'; import CiIcon from '../../../vue_shared/components/ci_icon.vue'; import Tabs from '../../../vue_shared/components/tabs/tabs'; import Tab from '../../../vue_shared/components/tabs/tab.vue'; @@ -14,7 +13,7 @@ import IDEServices from '~/ide/services'; export default { components: { - Icon, + GlIcon, CiIcon, Tabs, Tab, @@ -22,6 +21,9 @@ export default { EmptyState, GlLoadingIcon, }, + directives: { + SafeHtml, + }, computed: { ...mapState(['pipelinesEmptyStateSvgPath', 'links']), ...mapGetters(['currentProject']), @@ -70,7 +72,7 @@ export default { target="_blank" class="ide-external-link position-relative" > - #{{ latestPipeline.id }} <icon :size="12" name="external-link" /> + #{{ latestPipeline.id }} <gl-icon :size="12" name="external-link" /> </a> </span> </header> @@ -84,7 +86,7 @@ export default { <div v-else-if="latestPipeline.yamlError" class="bs-callout bs-callout-danger"> <p class="gl-mb-0">{{ __('Found errors in your .gitlab-ci.yml:') }}</p> <p class="gl-mb-0 break-word">{{ latestPipeline.yamlError }}</p> - <p class="gl-mb-0" v-html="ciLintText"></p> + <p v-safe-html="ciLintText" class="gl-mb-0"></p> </div> <tabs v-else class="ide-pipeline-list"> <tab :active="!pipelineFailed"> diff --git a/app/assets/javascripts/ide/components/preview/navigator.vue b/app/assets/javascripts/ide/components/preview/navigator.vue index 0de9dfd8827..60710251fef 100644 --- a/app/assets/javascripts/ide/components/preview/navigator.vue +++ b/app/assets/javascripts/ide/components/preview/navigator.vue @@ -1,11 +1,10 @@ <script> import { listen } from 'codesandbox-api'; -import { GlLoadingIcon } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; export default { components: { - Icon, + GlIcon, GlLoadingIcon, }, props: { @@ -97,7 +96,7 @@ export default { class="ide-navigator-btn d-flex align-items-center d-transparent border-0 bg-transparent" @click="back" > - <icon :size="24" name="chevron-left" class="m-auto" /> + <gl-icon :size="24" name="chevron-left" class="m-auto" /> </button> <button :aria-label="s__('IDE|Back')" @@ -109,7 +108,7 @@ export default { class="ide-navigator-btn d-flex align-items-center d-transparent border-0 bg-transparent" @click="forward" > - <icon :size="24" name="chevron-right" class="m-auto" /> + <gl-icon :size="24" name="chevron-right" class="m-auto" /> </button> <button :aria-label="s__('IDE|Refresh preview')" @@ -117,7 +116,7 @@ export default { class="ide-navigator-btn d-flex align-items-center d-transparent border-0 bg-transparent" @click="refresh" > - <icon :size="18" name="retry" class="m-auto" /> + <gl-icon :size="18" name="retry" class="m-auto" /> </button> <div class="position-relative w-100 gl-ml-2"> <input diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index d22d430cb4a..f342ce1739c 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -14,7 +14,7 @@ import Editor from '../lib/editor'; import FileTemplatesBar from './file_templates/bar.vue'; import { __ } from '~/locale'; import { extractMarkdownImagesFromEntries } from '../stores/utils'; -import { getPathParent, readFileAsDataURL } from '../utils'; +import { getPathParent, readFileAsDataURL, registerSchema, isTextFile } from '../utils'; import { getRulesWithTraversal } from '../lib/editorconfig/parser'; import mapRulesToMonaco from '../lib/editorconfig/rules_mapper'; @@ -48,6 +48,7 @@ export default { 'renderWhitespaceInCode', 'editorTheme', 'entries', + 'currentProjectId', ]), ...mapGetters([ 'currentMergeRequest', @@ -55,10 +56,11 @@ export default { 'isEditModeActive', 'isCommitModeActive', 'currentBranch', + 'getJsonSchemaForPath', ]), ...mapGetters('fileTemplates', ['showFileTemplatesBar']), shouldHideEditor() { - return this.file && this.file.binary; + return this.file && !isTextFile(this.file); }, showContentViewer() { return ( @@ -196,6 +198,8 @@ export default { this.editor.clearEditor(); + this.registerSchemaForFile(); + Promise.all([this.fetchFileData(), this.fetchEditorconfigRules()]) .then(() => { this.createEditorInstance(); @@ -329,6 +333,10 @@ export default { // do nothing if no image is found in the clipboard return Promise.resolve(); }, + registerSchemaForFile() { + const schema = this.getJsonSchemaForPath(this.file.path); + registerSchema(schema); + }, }, viewerTypes, FILE_VIEW_MODE_EDITOR, @@ -379,7 +387,7 @@ export default { :path="file.rawPath || file.path" :file-path="file.path" :file-size="file.size" - :project-path="file.projectId" + :project-path="currentProjectId" :commit-sha="currentBranchCommit" :type="fileType" /> @@ -390,7 +398,7 @@ export default { :new-sha="currentMergeRequest.sha" :old-path="file.mrChange.old_path" :old-sha="currentMergeRequest.baseCommitSha" - :project-path="file.projectId" + :project-path="currentProjectId" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/repo_file_status_icon.vue b/app/assets/javascripts/ide/components/repo_file_status_icon.vue index 9773e835a5c..1402f7aaf39 100644 --- a/app/assets/javascripts/ide/components/repo_file_status_icon.vue +++ b/app/assets/javascripts/ide/components/repo_file_status_icon.vue @@ -1,12 +1,12 @@ <script> +import { GlIcon } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; -import icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; import '~/lib/utils/datetime_utility'; export default { components: { - icon, + GlIcon, }, directives: { tooltip, @@ -29,6 +29,6 @@ export default { <template> <span v-if="file.file_lock" v-tooltip :title="lockTooltip" data-container="body"> - <icon name="lock" class="file-status-icon" /> + <gl-icon name="lock" class="file-status-icon" /> </span> </template> diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index 8370833233a..60a80a31a8b 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -1,9 +1,9 @@ <script> -import { mapActions } from 'vuex'; +import { mapActions, mapGetters } from 'vuex'; +import { GlIcon } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import FileIcon from '~/vue_shared/components/file_icon.vue'; -import Icon from '~/vue_shared/components/icon.vue'; import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; import FileStatusIcon from './repo_file_status_icon.vue'; @@ -11,7 +11,7 @@ export default { components: { FileStatusIcon, FileIcon, - Icon, + GlIcon, ChangedFileIcon, }, props: { @@ -26,6 +26,7 @@ export default { }; }, computed: { + ...mapGetters(['getUrlForPath']), closeLabel() { if (this.fileHasChanged) { return sprintf(__(`%{tabname} changed`), { tabname: this.tab.name }); @@ -52,7 +53,7 @@ export default { if (tab.pending) { this.openPendingTab({ file: tab, keyPrefix: tab.staged ? 'staged' : 'unstaged' }); } else { - this.$router.push(`/project${tab.url}`); + this.$router.push(this.getUrlForPath(tab.path)); } }, mouseOverTab() { @@ -79,7 +80,7 @@ export default { @mouseover="mouseOverTab" @mouseout="mouseOutTab" > - <div :title="tab.url" class="multi-file-tab"> + <div :title="getUrlForPath(tab.path)" class="multi-file-tab"> <file-icon :file-name="tab.name" :size="16" /> {{ tab.name }} <file-status-icon :file="tab" /> @@ -91,7 +92,7 @@ export default { class="multi-file-tab-close" @click.stop.prevent="closeFile(tab)" > - <icon v-if="!showChangedIcon" :size="12" name="close" /> + <gl-icon v-if="!showChangedIcon" :size="12" name="close" /> <changed-file-icon v-else :file="tab" /> </button> </li> diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue index 47c75be3f7c..c03694e3619 100644 --- a/app/assets/javascripts/ide/components/repo_tabs.vue +++ b/app/assets/javascripts/ide/components/repo_tabs.vue @@ -1,5 +1,5 @@ <script> -import { mapActions } from 'vuex'; +import { mapActions, mapGetters } from 'vuex'; import RepoTab from './repo_tab.vue'; export default { @@ -20,6 +20,9 @@ export default { required: true, }, }, + computed: { + ...mapGetters(['getUrlForPath']), + }, methods: { ...mapActions(['updateViewer', 'removePendingTab']), openFileViewer(viewer) { @@ -27,7 +30,7 @@ export default { if (this.activeFile.pending) { return this.removePendingTab(this.activeFile).then(() => { - this.$router.push(`/project${this.activeFile.url}`); + this.$router.push(this.getUrlForPath(this.activeFile.path)); }); } diff --git a/app/assets/javascripts/ide/components/shared/tokened_input.vue b/app/assets/javascripts/ide/components/shared/tokened_input.vue index de3e71dad92..e7a4c5487d1 100644 --- a/app/assets/javascripts/ide/components/shared/tokened_input.vue +++ b/app/assets/javascripts/ide/components/shared/tokened_input.vue @@ -1,10 +1,10 @@ <script> +import { GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; export default { components: { - Icon, + GlIcon, }, props: { placeholder: { @@ -81,7 +81,7 @@ export default { > <div class="value-container rounded"> <div class="value">{{ token.label }}</div> - <div class="remove-token inverted"><icon :size="10" name="close" /></div> + <div class="remove-token inverted"><gl-icon :size="10" name="close" /></div> </div> </button> </div> diff --git a/app/assets/javascripts/ide/components/terminal/empty_state.vue b/app/assets/javascripts/ide/components/terminal/empty_state.vue index 5dd12e62820..3668dd24e81 100644 --- a/app/assets/javascripts/ide/components/terminal/empty_state.vue +++ b/app/assets/javascripts/ide/components/terminal/empty_state.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { GlLoadingIcon } from '@gitlab/ui'; export default { diff --git a/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue b/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue index deb13b5615e..c3f722d6052 100644 --- a/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue +++ b/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue @@ -1,8 +1,7 @@ <script> import { throttle } from 'lodash'; -import { GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui'; +import { GlTooltipDirective, GlLoadingIcon, GlIcon } from '@gitlab/ui'; import { mapState } from 'vuex'; -import Icon from '~/vue_shared/components/icon.vue'; import { MSG_TERMINAL_SYNC_CONNECTING, MSG_TERMINAL_SYNC_UPLOADING, @@ -11,7 +10,7 @@ import { export default { components: { - Icon, + GlIcon, GlLoadingIcon, }, directives: { @@ -70,7 +69,7 @@ export default { <span>{{ __('Terminal') }}:</span> <span class="square s16 d-flex-center ml-1" :aria-label="status.text"> <gl-loading-icon v-if="isLoading" inline size="sm" class="d-flex-center" /> - <icon v-else-if="status.icon" :name="status.icon" :size="16" /> + <gl-icon v-else-if="status.icon" :name="status.icon" :size="16" /> </span> </div> </template> diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js index 82cf8d7a10a..396aedbfa10 100644 --- a/app/assets/javascripts/ide/ide_router.js +++ b/app/assets/javascripts/ide/ide_router.js @@ -33,7 +33,6 @@ const EmptyRouterComponent = { }, }; -// eslint-disable-next-line import/prefer-default-export export const createRouter = store => { const router = new IdeRouter({ mode: 'history', diff --git a/app/assets/javascripts/ide/lib/diff/diff.js b/app/assets/javascripts/ide/lib/diff/diff.js index 3a456b7c4d6..62ec798b372 100644 --- a/app/assets/javascripts/ide/lib/diff/diff.js +++ b/app/assets/javascripts/ide/lib/diff/diff.js @@ -1,8 +1,6 @@ import { diffLines } from 'diff'; import { defaultDiffOptions } from '../editor_options'; -// See: https://gitlab.com/gitlab-org/frontend/rfcs/-/issues/20 -// eslint-disable-next-line import/prefer-default-export export const computeDiff = (originalContent, newContent) => { // prevent EOL changes from highlighting the entire file const changes = diffLines( diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js index f061fcb1259..2b12230c7cd 100644 --- a/app/assets/javascripts/ide/lib/editor.js +++ b/app/assets/javascripts/ide/lib/editor.js @@ -7,10 +7,9 @@ import ModelManager from './common/model_manager'; import { editorOptions, defaultEditorOptions, defaultDiffEditorOptions } from './editor_options'; import { themes } from './themes'; import languages from './languages'; -import schemas from './schemas'; import keymap from './keymap.json'; import { clearDomElement } from '~/editor/utils'; -import { registerLanguages, registerSchemas } from '../utils'; +import { registerLanguages } from '../utils'; function setupThemes() { themes.forEach(theme => { @@ -46,10 +45,6 @@ export default class Editor { setupThemes(); registerLanguages(...languages); - if (gon.features?.schemaLinting) { - registerSchemas(...schemas); - } - this.debouncedUpdate = debounce(() => { this.updateDimensions(); }, 200); diff --git a/app/assets/javascripts/ide/lib/editorconfig/parser.js b/app/assets/javascripts/ide/lib/editorconfig/parser.js index a30a8cb868d..1597e4a8bfa 100644 --- a/app/assets/javascripts/ide/lib/editorconfig/parser.js +++ b/app/assets/javascripts/ide/lib/editorconfig/parser.js @@ -42,7 +42,6 @@ function getRulesWithConfigs(filePath, configFiles = [], rules = {}) { return isRoot ? result : getRulesWithConfigs(filePath, nextConfigs, result); } -// eslint-disable-next-line import/prefer-default-export export function getRulesWithTraversal(filePath, getFileContent) { const editorconfigPaths = [ ...getPathParents(filePath).map(x => `${x}/.editorconfig`), diff --git a/app/assets/javascripts/ide/lib/errors.js b/app/assets/javascripts/ide/lib/errors.js new file mode 100644 index 00000000000..6ae18bc8180 --- /dev/null +++ b/app/assets/javascripts/ide/lib/errors.js @@ -0,0 +1,39 @@ +import { escape } from 'lodash'; +import { __ } from '~/locale'; + +const CODEOWNERS_REGEX = /Push.*protected branches.*CODEOWNERS/; +const BRANCH_CHANGED_REGEX = /changed.*since.*start.*edit/; + +export const createUnexpectedCommitError = () => ({ + title: __('Unexpected error'), + messageHTML: __('Could not commit. An unexpected error occurred.'), + canCreateBranch: false, +}); + +export const createCodeownersCommitError = message => ({ + title: __('CODEOWNERS rule violation'), + messageHTML: escape(message), + canCreateBranch: true, +}); + +export const createBranchChangedCommitError = message => ({ + title: __('Branch changed'), + messageHTML: `${escape(message)}<br/><br/>${__('Would you like to create a new branch?')}`, + canCreateBranch: true, +}); + +export const parseCommitError = e => { + const { message } = e?.response?.data || {}; + + if (!message) { + return createUnexpectedCommitError(); + } + + if (CODEOWNERS_REGEX.test(message)) { + return createCodeownersCommitError(message); + } else if (BRANCH_CHANGED_REGEX.test(message)) { + return createBranchChangedCommitError(message); + } + + return createUnexpectedCommitError(); +}; diff --git a/app/assets/javascripts/ide/lib/files.js b/app/assets/javascripts/ide/lib/files.js index 6d85e225fd5..789e09fa8f2 100644 --- a/app/assets/javascripts/ide/lib/files.js +++ b/app/assets/javascripts/ide/lib/files.js @@ -1,4 +1,3 @@ -import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils'; import { decorateData, sortTree } from '../stores/utils'; export const splitParent = path => { @@ -13,15 +12,7 @@ export const splitParent = path => { /** * Create file objects from a list of file paths. */ -export const decorateFiles = ({ - data, - projectId, - branchId, - tempFile = false, - content = '', - binary = false, - rawPath = '', -}) => { +export const decorateFiles = ({ data, tempFile = false, content = '', rawPath = '' }) => { const treeList = []; const entries = {}; @@ -41,12 +32,9 @@ export const decorateFiles = ({ parentPath = parentFolder && parentFolder.path; const tree = decorateData({ - projectId, - branchId, id: path, name, path, - url: `/${projectId}/tree/${branchId}/-/${path}/`, type: 'tree', tempFile, changed: tempFile, @@ -73,21 +61,16 @@ export const decorateFiles = ({ const fileFolder = parent && insertParent(parent); if (name) { - const previewMode = viewerInformationForPath(name); parentPath = fileFolder && fileFolder.path; file = decorateData({ - projectId, - branchId, id: path, name, path, - url: `/${projectId}/blob/${branchId}/-/${path}`, type: 'blob', tempFile, changed: tempFile, content, - binary: (previewMode && previewMode.binary) || binary, rawPath, parentPath, }); diff --git a/app/assets/javascripts/ide/lib/schemas/index.js b/app/assets/javascripts/ide/lib/schemas/index.js deleted file mode 100644 index 38a2f81921b..00000000000 --- a/app/assets/javascripts/ide/lib/schemas/index.js +++ /dev/null @@ -1,4 +0,0 @@ -import json from './json'; -import yaml from './yaml'; - -export default [json, yaml]; diff --git a/app/assets/javascripts/ide/lib/schemas/json/index.js b/app/assets/javascripts/ide/lib/schemas/json/index.js deleted file mode 100644 index 900d5442bec..00000000000 --- a/app/assets/javascripts/ide/lib/schemas/json/index.js +++ /dev/null @@ -1,8 +0,0 @@ -export default { - language: 'json', - options: { - validate: true, - enableSchemaRequest: true, - schemas: [], - }, -}; diff --git a/app/assets/javascripts/ide/lib/schemas/yaml/gitlab_ci.js b/app/assets/javascripts/ide/lib/schemas/yaml/gitlab_ci.js deleted file mode 100644 index af20744abb3..00000000000 --- a/app/assets/javascripts/ide/lib/schemas/yaml/gitlab_ci.js +++ /dev/null @@ -1,4 +0,0 @@ -export default { - uri: 'https://json.schemastore.org/gitlab-ci', - fileMatch: ['*.gitlab-ci.yml'], -}; diff --git a/app/assets/javascripts/ide/lib/schemas/yaml/index.js b/app/assets/javascripts/ide/lib/schemas/yaml/index.js deleted file mode 100644 index e3fc406df4b..00000000000 --- a/app/assets/javascripts/ide/lib/schemas/yaml/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import gitlabCi from './gitlab_ci'; - -export default { - language: 'yaml', - options: { - validate: true, - enableSchemaRequest: true, - hover: true, - completion: true, - schemas: [gitlabCi], - }, -}; diff --git a/app/assets/javascripts/ide/services/gql.js b/app/assets/javascripts/ide/services/gql.js index 211cc78bd99..89dda187360 100644 --- a/app/assets/javascripts/ide/services/gql.js +++ b/app/assets/javascripts/ide/services/gql.js @@ -17,5 +17,4 @@ const getClient = memoize(() => ), ); -// eslint-disable-next-line import/prefer-default-export export const query = (...args) => getClient().query(...args); diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index ae4a1ba3db5..70a6a6b423d 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -33,7 +33,7 @@ export default { }) .then(({ data }) => data); }, - getBaseRawFileData(file, sha) { + getBaseRawFileData(file, projectId, ref) { if (file.tempFile || file.baseRaw) return Promise.resolve(file.baseRaw); // if files are renamed, their base path has changed @@ -44,10 +44,10 @@ export default { .get( joinPaths( gon.relative_url_root || '/', - file.projectId, + projectId, '-', 'raw', - sha, + ref, escapeFileUrl(filePath), ), { diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index b083dc6325f..b8d59f8bd36 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -25,15 +25,7 @@ export const setResizingStatus = ({ commit }, resizing) => { export const createTempEntry = ( { state, commit, dispatch, getters }, - { - name, - type, - content = '', - binary = false, - rawPath = '', - openFile = true, - makeFileActive = true, - }, + { name, type, content = '', rawPath = '', openFile = true, makeFileActive = true }, ) => { const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name; @@ -54,21 +46,14 @@ export const createTempEntry = ( const data = decorateFiles({ data: [fullName], - projectId: state.currentProjectId, - branchId: state.currentBranchId, type, tempFile: true, content, - binary, rawPath, }); const { file, parentPath } = data; - commit(types.CREATE_TMP_ENTRY, { - data, - projectId: state.currentProjectId, - branchId: state.currentBranchId, - }); + commit(types.CREATE_TMP_ENTRY, { data }); if (type === 'blob') { if (openFile) commit(types.TOGGLE_FILE_OPEN, file.path); @@ -90,7 +75,6 @@ export const addTempImage = ({ dispatch, getters }, { name, rawPath = '' }) => name: getters.getAvailableFileName(name), type: 'blob', content: rawPath.split('base64,')[1], - binary: true, rawPath, openFile: false, makeFileActive: false, @@ -254,7 +238,7 @@ export const renameEntry = ({ dispatch, commit, state, getters }, { path, name, } if (newEntry.opened) { - dispatch('router/push', `/project${newEntry.url}`, { root: true }); + dispatch('router/push', getters.getUrlForPath(newEntry.path), { root: true }); } } diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index c0cb924e749..3515d1fc933 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -6,7 +6,7 @@ import * as types from '../mutation_types'; import { setPageTitleForFile } from '../utils'; import { viewerTypes, stageKeys } from '../../constants'; -export const closeFile = ({ commit, state, dispatch }, file) => { +export const closeFile = ({ commit, state, dispatch, getters }, file) => { const { path } = file; const indexOfClosedFile = state.openFiles.findIndex(f => f.key === file.key); const fileWasActive = file.active; @@ -29,10 +29,12 @@ export const closeFile = ({ commit, state, dispatch }, file) => { keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged', }); } else { - dispatch('router/push', `/project${nextFileToOpen.url}`, { root: true }); + dispatch('router/push', getters.getUrlForPath(nextFileToOpen.path), { root: true }); } } else if (!state.openFiles.length) { - dispatch('router/push', `/project/${file.projectId}/tree/${file.branchId}/`, { root: true }); + dispatch('router/push', `/project/${state.currentProjectId}/tree/${state.currentBranchId}/`, { + root: true, + }); } eventHub.$emit(`editor.update.model.dispose.${file.key}`); @@ -121,7 +123,7 @@ export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) = const baseSha = (getters.currentMergeRequest && getters.currentMergeRequest.baseCommitSha) || ''; - return service.getBaseRawFileData(file, baseSha).then(baseRaw => { + return service.getBaseRawFileData(file, state.currentProjectId, baseSha).then(baseRaw => { commit(types.SET_FILE_BASE_RAW_DATA, { file, baseRaw, @@ -218,7 +220,7 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) = if (!isDestructiveDiscard && file.path === getters.activeFile?.path) { dispatch('updateDelayViewerUpdated', true) .then(() => { - dispatch('router/push', `/project${file.url}`, { root: true }); + dispatch('router/push', getters.getUrlForPath(file.path), { root: true }); }) .catch(e => { throw e; @@ -274,7 +276,7 @@ export const openPendingTab = ({ commit, dispatch, getters, state }, { file, key commit(types.ADD_PENDING_TAB, { file, keyPrefix }); - dispatch('router/push', `/project/${file.projectId}/tree/${state.currentBranchId}/`, { + dispatch('router/push', `/project/${state.currentProjectId}/tree/${state.currentBranchId}/`, { root: true, }); diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js index 1ca608f1287..3a7daf30cc4 100644 --- a/app/assets/javascripts/ide/stores/actions/tree.js +++ b/app/assets/javascripts/ide/stores/actions/tree.js @@ -61,11 +61,7 @@ export const getFiles = ({ state, commit, dispatch }, payload = {}) => service .getFiles(selectedProject.path_with_namespace, ref) .then(({ data }) => { - const { entries, treeList } = decorateFiles({ - data, - projectId, - branchId, - }); + const { entries, treeList } = decorateFiles({ data }); commit(types.SET_ENTRIES, entries); diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index 53734fa626b..b8304a9b68d 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -6,6 +6,7 @@ import { PERMISSION_CREATE_MR, PERMISSION_PUSH_CODE, } from '../constants'; +import Api from '~/api'; export const activeFile = state => state.openFiles.find(file => file.active) || null; @@ -174,3 +175,21 @@ export const getAvailableFileName = (state, getters) => path => { return newPath; }; + +export const getUrlForPath = state => path => + `/project/${state.currentProjectId}/tree/${state.currentBranchId}/-/${path}/`; + +export const getJsonSchemaForPath = (state, getters) => path => { + const [namespace, ...project] = state.currentProjectId.split('/'); + return { + uri: + // eslint-disable-next-line no-restricted-globals + location.origin + + Api.buildUrl(Api.projectFileSchemaPath) + .replace(':namespace_path', namespace) + .replace(':project_path', project.join('/')) + .replace(':ref', getters.currentBranch?.commit.id || state.currentBranchId) + .replace(':filename', path), + fileMatch: [`*${path}`], + }; +}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index 277e6923f17..90a6c644d17 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -1,6 +1,5 @@ import { sprintf, __ } from '~/locale'; import { deprecatedCreateFlash as flash } from '~/flash'; -import httpStatusCodes from '~/lib/utils/http_status'; import * as rootTypes from '../../mutation_types'; import { createCommitPayload, createNewMergeRequestUrl } from '../../utils'; import service from '../../../services'; @@ -8,6 +7,7 @@ import * as types from './mutation_types'; import consts from './constants'; import { leftSidebarViews } from '../../../constants'; import eventHub from '../../../eventhub'; +import { parseCommitError } from '../../../lib/errors'; export const updateCommitMessage = ({ commit }, message) => { commit(types.UPDATE_COMMIT_MESSAGE, message); @@ -113,6 +113,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo ? Promise.resolve() : dispatch('stageAllChanges', null, { root: true }); + commit(types.CLEAR_ERROR); commit(types.UPDATE_LOADING, true); return stageFilesPromise @@ -128,6 +129,12 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo return service.commit(rootState.currentProjectId, payload); }) + .catch(e => { + commit(types.UPDATE_LOADING, false); + commit(types.SET_ERROR, parseCommitError(e)); + + throw e; + }) .then(({ data }) => { commit(types.UPDATE_LOADING, false); @@ -214,24 +221,5 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo { root: true }, ), ); - }) - .catch(err => { - commit(types.UPDATE_LOADING, false); - - // don't catch bad request errors, let the view handle them - if (err.response.status === httpStatusCodes.BAD_REQUEST) throw err; - - dispatch( - 'setErrorMessage', - { - text: __('An error occurred while committing your changes.'), - action: () => - dispatch('commitChanges').then(() => dispatch('setErrorMessage', null, { root: true })), - actionText: __('Please try again'), - }, - { root: true }, - ); - - window.dispatchEvent(new Event('resize')); }); }; diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js index 7ad8f3570b7..47ec2ffbdde 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js +++ b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js @@ -3,3 +3,6 @@ export const UPDATE_COMMIT_ACTION = 'UPDATE_COMMIT_ACTION'; export const UPDATE_NEW_BRANCH_NAME = 'UPDATE_NEW_BRANCH_NAME'; export const UPDATE_LOADING = 'UPDATE_LOADING'; export const TOGGLE_SHOULD_CREATE_MR = 'TOGGLE_SHOULD_CREATE_MR'; + +export const CLEAR_ERROR = 'CLEAR_ERROR'; +export const SET_ERROR = 'SET_ERROR'; diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutations.js b/app/assets/javascripts/ide/stores/modules/commit/mutations.js index 73b618e250f..2cf6e8e6f36 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/commit/mutations.js @@ -24,4 +24,10 @@ export default { shouldCreateMR: shouldCreateMR === undefined ? !state.shouldCreateMR : shouldCreateMR, }); }, + [types.CLEAR_ERROR](state) { + state.commitError = null; + }, + [types.SET_ERROR](state, error) { + state.commitError = error; + }, }; diff --git a/app/assets/javascripts/ide/stores/modules/commit/state.js b/app/assets/javascripts/ide/stores/modules/commit/state.js index f49737485f2..de092a569ad 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/state.js +++ b/app/assets/javascripts/ide/stores/modules/commit/state.js @@ -4,4 +4,5 @@ export default () => ({ newBranchName: '', submitCommitLoading: false, shouldCreateMR: true, + commitError: null, }); diff --git a/app/assets/javascripts/ide/stores/modules/pane/getters.js b/app/assets/javascripts/ide/stores/modules/pane/getters.js index 7816172bb6f..ce597329df1 100644 --- a/app/assets/javascripts/ide/stores/modules/pane/getters.js +++ b/app/assets/javascripts/ide/stores/modules/pane/getters.js @@ -1,3 +1,2 @@ -// eslint-disable-next-line import/prefer-default-export 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 86b889546b0..99bd08ee876 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js @@ -118,31 +118,31 @@ export const setDetailJob = ({ commit, dispatch }, job) => { }); }; -export const requestJobTrace = ({ commit }) => commit(types.REQUEST_JOB_TRACE); -export const receiveJobTraceError = ({ commit, dispatch }) => { +export const requestJobLogs = ({ commit }) => commit(types.REQUEST_JOB_LOGS); +export const receiveJobLogsError = ({ commit, dispatch }) => { dispatch( 'setErrorMessage', { - text: __('An error occurred while fetching the job trace.'), + text: __('An error occurred while fetching the job logs.'), action: () => - dispatch('fetchJobTrace').then(() => dispatch('setErrorMessage', null, { root: true })), + dispatch('fetchJobLogs').then(() => dispatch('setErrorMessage', null, { root: true })), actionText: __('Please try again'), actionPayload: null, }, { root: true }, ); - commit(types.RECEIVE_JOB_TRACE_ERROR); + commit(types.RECEIVE_JOB_LOGS_ERROR); }; -export const receiveJobTraceSuccess = ({ commit }, data) => - commit(types.RECEIVE_JOB_TRACE_SUCCESS, data); +export const receiveJobLogsSuccess = ({ commit }, data) => + commit(types.RECEIVE_JOB_LOGS_SUCCESS, data); -export const fetchJobTrace = ({ dispatch, state }) => { - dispatch('requestJobTrace'); +export const fetchJobLogs = ({ dispatch, state }) => { + dispatch('requestJobLogs'); return axios .get(`${state.detailJob.path}/trace`, { params: { format: 'json' } }) - .then(({ data }) => dispatch('receiveJobTraceSuccess', data)) - .catch(() => dispatch('receiveJobTraceError')); + .then(({ data }) => dispatch('receiveJobLogsSuccess', data)) + .catch(() => dispatch('receiveJobLogsError')); }; export const resetLatestPipeline = ({ commit }) => { diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/constants.js b/app/assets/javascripts/ide/stores/modules/pipelines/constants.js index f5b96327e40..bb4145934ff 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/constants.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/constants.js @@ -1,4 +1,3 @@ -// eslint-disable-next-line import/prefer-default-export export const states = { failed: 'failed', }; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js index f4c36b9d96f..fea3055e0fe 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js @@ -10,6 +10,6 @@ export const TOGGLE_STAGE_COLLAPSE = 'TOGGLE_STAGE_COLLAPSE'; export const SET_DETAIL_JOB = 'SET_DETAIL_JOB'; -export const REQUEST_JOB_TRACE = 'REQUEST_JOB_TRACE'; -export const RECEIVE_JOB_TRACE_ERROR = 'RECEIVE_JOB_TRACE_ERROR'; -export const RECEIVE_JOB_TRACE_SUCCESS = 'RECEIVE_JOB_TRACE_SUCCESS'; +export const REQUEST_JOB_LOGS = 'REQUEST_JOB_LOGS'; +export const RECEIVE_JOB_LOGS_ERROR = 'RECEIVE_JOB_LOGS_ERROR'; +export const RECEIVE_JOB_LOGS_SUCCESS = 'RECEIVE_JOB_LOGS_SUCCESS'; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js index eaaa82cb339..3a3cb4a7cb2 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js @@ -66,13 +66,13 @@ export default { [types.SET_DETAIL_JOB](state, job) { state.detailJob = { ...job }; }, - [types.REQUEST_JOB_TRACE](state) { + [types.REQUEST_JOB_LOGS](state) { state.detailJob.isLoading = true; }, - [types.RECEIVE_JOB_TRACE_ERROR](state) { + [types.RECEIVE_JOB_LOGS_ERROR](state) { state.detailJob.isLoading = false; }, - [types.RECEIVE_JOB_TRACE_SUCCESS](state, data) { + [types.RECEIVE_JOB_LOGS_SUCCESS](state, data) { state.detailJob.isLoading = false; state.detailJob.output = data.html; }, diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js index a6caca2d2dc..95716e0a0c6 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js @@ -1,4 +1,3 @@ -// eslint-disable-next-line import/prefer-default-export export const normalizeJob = job => ({ id: job.id, name: job.name, diff --git a/app/assets/javascripts/ide/stores/modules/router/actions.js b/app/assets/javascripts/ide/stores/modules/router/actions.js index 849067599f2..321ac57817f 100644 --- a/app/assets/javascripts/ide/stores/modules/router/actions.js +++ b/app/assets/javascripts/ide/stores/modules/router/actions.js @@ -1,6 +1,5 @@ import * as types from './mutation_types'; -// eslint-disable-next-line import/prefer-default-export export const push = ({ commit }, fullPath) => { commit(types.PUSH, fullPath); }; diff --git a/app/assets/javascripts/ide/stores/modules/router/mutation_types.js b/app/assets/javascripts/ide/stores/modules/router/mutation_types.js index ae99073cc4c..8f5f949bd5f 100644 --- a/app/assets/javascripts/ide/stores/modules/router/mutation_types.js +++ b/app/assets/javascripts/ide/stores/modules/router/mutation_types.js @@ -1,2 +1 @@ -// eslint-disable-next-line import/prefer-default-export export const PUSH = 'PUSH'; diff --git a/app/assets/javascripts/ide/stores/modules/terminal/getters.js b/app/assets/javascripts/ide/stores/modules/terminal/getters.js index ef98547ccc4..b29d391845d 100644 --- a/app/assets/javascripts/ide/stores/modules/terminal/getters.js +++ b/app/assets/javascripts/ide/stores/modules/terminal/getters.js @@ -1,4 +1,3 @@ -// eslint-disable-next-line import/prefer-default-export export const allCheck = state => { const checks = Object.values(state.checks); diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index c64839e5019..460d3ced381 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -7,7 +7,6 @@ import treeMutations from './mutations/tree'; import branchMutations from './mutations/branch'; import { sortTree, - replaceFileUrl, swapInParentTreeWithSorting, updateFileCollections, removeFromParentTree, @@ -49,7 +48,7 @@ export default { entries, }); }, - [types.CREATE_TMP_ENTRY](state, { data, projectId, branchId }) { + [types.CREATE_TMP_ENTRY](state, { data }) { Object.keys(data.entries).reduce((acc, key) => { const entry = data.entries[key]; const foundEntry = state.entries[key]; @@ -72,13 +71,12 @@ export default { return acc.concat(key); }, []); - const foundEntry = state.trees[`${projectId}/${branchId}`].tree.find( - e => e.path === data.treeList[0].path, - ); + const currentTree = state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; + const foundEntry = currentTree.tree.find(e => e.path === data.treeList[0].path); if (!foundEntry) { - Object.assign(state.trees[`${projectId}/${branchId}`], { - tree: sortTree(state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList)), + Object.assign(currentTree, { + tree: sortTree(currentTree.tree.concat(data.treeList)), }); } }, @@ -139,7 +137,6 @@ export default { prevId: undefined, prevPath: undefined, prevName: undefined, - prevUrl: undefined, prevKey: undefined, prevParentPath: undefined, }); @@ -195,9 +192,6 @@ export default { const oldEntry = state.entries[path]; const newPath = parentPath ? `${parentPath}/${name}` : name; const isRevert = newPath === oldEntry.prevPath; - - const newUrl = replaceFileUrl(oldEntry.url, oldEntry.path, newPath); - const newKey = oldEntry.key.replace(new RegExp(oldEntry.path, 'g'), newPath); const baseProps = { @@ -205,7 +199,6 @@ export default { name, id: newPath, path: newPath, - url: newUrl, key: newKey, parentPath: parentPath || '', }; @@ -216,7 +209,6 @@ export default { prevId: undefined, prevPath: undefined, prevName: undefined, - prevUrl: undefined, prevKey: undefined, prevParentPath: undefined, } @@ -224,7 +216,6 @@ export default { prevId: oldEntry.prevId || oldEntry.id, prevPath: oldEntry.prevPath || oldEntry.path, prevName: oldEntry.prevName || oldEntry.name, - prevUrl: oldEntry.prevUrl || oldEntry.url, prevKey: oldEntry.prevKey || oldEntry.key, prevParentPath: oldEntry.prevParentPath || oldEntry.parentPath, }; diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index f074e6880d0..d9cdc7727ad 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -12,10 +12,7 @@ export const dataStructure = () => ({ // it can also contain a prefix `pending-` for files opened in review mode key: '', type: '', - projectId: '', - branchId: '', name: '', - url: '', path: '', tempFile: false, tree: [], @@ -26,7 +23,6 @@ export const dataStructure = () => ({ staged: false, lastCommitSha: '', rawPath: '', - binary: false, raw: '', content: '', editorRow: 1, @@ -44,10 +40,7 @@ export const dataStructure = () => ({ export const decorateData = entity => { const { id, - projectId, - branchId, type, - url, name, path, content = '', @@ -55,7 +48,6 @@ export const decorateData = entity => { active = false, opened = false, changed = false, - binary = false, rawPath = '', file_lock, parentPath = '', @@ -63,19 +55,15 @@ export const decorateData = entity => { return Object.assign(dataStructure(), { id, - projectId, - branchId, key: `${name}-${type}-${id}`, type, name, - url, path, tempFile, opened, active, changed, content, - binary, rawPath, file_lock, parentPath, @@ -189,11 +177,6 @@ export const mergeTrees = (fromTree, toTree) => { return toTree; }; -export const replaceFileUrl = (url, oldPath, newPath) => { - // Add `/-/` so that we don't accidentally replace project path - return url.replace(`/-/${oldPath}`, `/-/${newPath}`); -}; - export const swapInStateArray = (state, arr, key, entryPath) => Object.assign(state, { [arr]: state[arr].map(f => (f.key === key ? state.entries[entryPath] : f)), diff --git a/app/assets/javascripts/ide/sync_router_and_store.js b/app/assets/javascripts/ide/sync_router_and_store.js index 1782c32b3b2..b33bcbb94ea 100644 --- a/app/assets/javascripts/ide/sync_router_and_store.js +++ b/app/assets/javascripts/ide/sync_router_and_store.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ /** * This method adds listeners to the given router and store and syncs their state with eachother * diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js index 58a6712c232..cde53e1ef00 100644 --- a/app/assets/javascripts/ide/utils.js +++ b/app/assets/javascripts/ide/utils.js @@ -1,5 +1,5 @@ import { languages } from 'monaco-editor'; -import { flatten } from 'lodash'; +import { flatten, isString } from 'lodash'; import { SIDE_LEFT, SIDE_RIGHT } from './constants'; const toLowerCase = x => x.toLowerCase(); @@ -42,15 +42,16 @@ const KNOWN_TYPES = [ }, ]; -export function isTextFile(content, mimeType, fileName) { - const knownType = KNOWN_TYPES.find(type => type.isMatch(mimeType, fileName)); +export function isTextFile({ name, content, mimeType = '' }) { + 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) const asciiRegex = /^[ -~\t\n\r]+$/; + // for unknown types, determine the type by evaluating the file contents - return asciiRegex.test(content); + return isString(content) && (content === '' || asciiRegex.test(content)); } export const createPathWithExt = p => { @@ -75,17 +76,17 @@ export function registerLanguages(def, ...defs) { languages.setLanguageConfiguration(languageId, def.conf); } -export function registerSchemas({ language, options }, ...schemas) { - schemas.forEach(schema => registerSchemas(schema)); - - const defaults = { - json: languages.json.jsonDefaults, - yaml: languages.yaml.yamlDefaults, - }; - - if (defaults[language]) { - defaults[language].setDiagnosticsOptions(options); - } +export function registerSchema(schema) { + const defaults = [languages.json.jsonDefaults, languages.yaml.yamlDefaults]; + defaults.forEach(d => + d.setDiagnosticsOptions({ + validate: true, + enableSchemaRequest: true, + hover: true, + completion: true, + schemas: [schema], + }), + ); } export const otherSide = side => (side === SIDE_RIGHT ? SIDE_LEFT : SIDE_RIGHT); diff --git a/app/assets/javascripts/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_projects/components/import_projects_table.vue index 72fdaca7e24..96100e4ac0c 100644 --- a/app/assets/javascripts/import_projects/components/import_projects_table.vue +++ b/app/assets/javascripts/import_projects/components/import_projects_table.vue @@ -1,27 +1,17 @@ <script> -import { throttle } from 'lodash'; import { mapActions, mapState, mapGetters } from 'vuex'; -import { GlButton, GlLoadingIcon } from '@gitlab/ui'; -import { __, sprintf } from '~/locale'; -import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; -import ImportedProjectTableRow from './imported_project_table_row.vue'; +import { GlButton, GlLoadingIcon, GlIntersectionObserver, GlModal } from '@gitlab/ui'; +import { n__, __, sprintf } from '~/locale'; import ProviderRepoTableRow from './provider_repo_table_row.vue'; -import IncompatibleRepoTableRow from './incompatible_repo_table_row.vue'; -import PageQueryParamSync from './page_query_param_sync.vue'; -import { isProjectImportable } from '../utils'; - -const reposFetchThrottleDelay = 1000; export default { name: 'ImportProjectsTable', components: { - ImportedProjectTableRow, ProviderRepoTableRow, - IncompatibleRepoTableRow, - PageQueryParamSync, GlLoadingIcon, GlButton, - PaginationLinks, + GlModal, + GlIntersectionObserver, }, props: { providerTitle: { @@ -41,14 +31,19 @@ export default { }, computed: { - ...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace', 'pageInfo']), + ...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace']), ...mapGetters([ 'isLoading', 'isImportingAnyRepo', 'hasImportableRepos', 'hasIncompatibleRepos', + 'importAllCount', ]), + pagePaginationStateKey() { + return `${this.filter}-${this.repositories.length}`; + }, + availableNamespaces() { const serializedNamespaces = this.namespaces.map(({ fullPath }) => ({ id: fullPath, @@ -66,8 +61,12 @@ export default { importAllButtonText() { return this.hasIncompatibleRepos - ? __('Import all compatible repositories') - : __('Import all repositories'); + ? n__( + 'Import %d compatible repository', + 'Import %d compatible repositories', + this.importAllCount, + ) + : n__('Import %d repository', 'Import %d repositories', this.importAllCount); }, emptyStateText() { @@ -83,7 +82,11 @@ export default { mounted() { this.fetchNamespaces(); - this.fetchRepos(); + this.fetchJobs(); + + if (!this.paginatable) { + this.fetchRepos(); + } }, beforeDestroy() { @@ -94,105 +97,95 @@ export default { methods: { ...mapActions([ 'fetchRepos', + 'fetchJobs', 'fetchNamespaces', 'stopJobsPolling', 'clearJobsEtagPoll', 'setFilter', 'importAll', - 'setPage', ]), - - handleFilterInput({ target }) { - this.setFilter(target.value); - }, - - throttledFetchRepos: throttle(function fetch() { - this.fetchRepos(); - }, reposFetchThrottleDelay), - - isProjectImportable, }, }; </script> <template> <div> - <page-query-param-sync :page="pageInfo.page" @popstate="setPage" /> - <p class="light text-nowrap mt-2"> - {{ s__('ImportProjects|Select the projects you want to import') }} + {{ s__('ImportProjects|Select the repositories you want to import') }} </p> <template v-if="hasIncompatibleRepos"> <slot name="incompatible-repos-warning"></slot> </template> + <div class="d-flex justify-content-between align-items-end flex-wrap mb-3"> + <gl-button + variant="success" + :loading="isImportingAnyRepo" + :disabled="!hasImportableRepos" + type="button" + @click="$refs.importAllModal.show()" + >{{ importAllButtonText }}</gl-button + > + <gl-modal + ref="importAllModal" + modal-id="import-all-modal" + :title="s__('ImportProjects|Import repositories')" + :ok-title="__('Import')" + @ok="importAll" + > + {{ + n__( + 'Are you sure you want to import %d repository?', + 'Are you sure you want to import %d repositories?', + importAllCount, + ) + }} + </gl-modal> + + <slot name="actions"></slot> + <form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent> + <input + data-qa-selector="githubish_import_filter_field" + class="form-control" + name="filter" + :placeholder="__('Filter your repositories by name')" + autofocus + size="40" + @keyup.enter="setFilter($event.target.value)" + /> + </form> + </div> + <div v-if="repositories.length" class="table-responsive"> + <table class="table import-table"> + <thead> + <th class="import-jobs-from-col">{{ fromHeaderText }}</th> + <th class="import-jobs-to-col">{{ __('To GitLab') }}</th> + <th class="import-jobs-status-col">{{ __('Status') }}</th> + <th class="import-jobs-cta-col"></th> + </thead> + <tbody> + <template v-for="repo in repositories"> + <provider-repo-table-row + :key="repo.importSource.providerLink" + :repo="repo" + :available-namespaces="availableNamespaces" + /> + </template> + </tbody> + </table> + </div> + <gl-intersection-observer + v-if="paginatable" + :key="pagePaginationStateKey" + @appear="fetchRepos" + /> <gl-loading-icon v-if="isLoading" class="js-loading-button-icon import-projects-loading-icon" size="md" /> - <template v-if="!isLoading"> - <div class="d-flex justify-content-between align-items-end flex-wrap mb-3"> - <gl-button - variant="success" - :loading="isImportingAnyRepo" - :disabled="!hasImportableRepos" - type="button" - @click="importAll" - >{{ importAllButtonText }}</gl-button - > - <slot name="actions"></slot> - <form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent> - <input - :value="filter" - data-qa-selector="githubish_import_filter_field" - class="form-control" - name="filter" - :placeholder="__('Filter your projects by name')" - autofocus - size="40" - @input="handleFilterInput($event)" - @keyup.enter="throttledFetchRepos" - /> - </form> - </div> - <div v-if="repositories.length" class="table-responsive"> - <table class="table import-table"> - <thead> - <th class="import-jobs-from-col">{{ fromHeaderText }}</th> - <th class="import-jobs-to-col">{{ __('To GitLab') }}</th> - <th class="import-jobs-status-col">{{ __('Status') }}</th> - <th class="import-jobs-cta-col"></th> - </thead> - <tbody> - <template v-for="repo in repositories"> - <incompatible-repo-table-row - v-if="repo.importSource.incompatible" - :key="repo.importSource.id" - :repo="repo" - /> - <provider-repo-table-row - v-else-if="isProjectImportable(repo)" - :key="repo.importSource.id" - :repo="repo" - :available-namespaces="availableNamespaces" - /> - <imported-project-table-row v-else :key="repo.importSource.id" :project="repo" /> - </template> - </tbody> - </table> - </div> - <div v-else class="text-center"> - <strong>{{ emptyStateText }}</strong> - </div> - <pagination-links - v-if="paginatable" - align="center" - class="gl-mt-3" - :page-info="pageInfo" - :prev-page="pageInfo.page - 1" - :next-page="repositories.length && pageInfo.page + 1" - :change="setPage" - /> - </template> + + <div v-if="!isLoading && repositories.length === 0" class="text-center"> + <strong>{{ emptyStateText }}</strong> + </div> </div> </template> diff --git a/app/assets/javascripts/import_projects/components/imported_project_table_row.vue b/app/assets/javascripts/import_projects/components/imported_project_table_row.vue deleted file mode 100644 index 50e735b4478..00000000000 --- a/app/assets/javascripts/import_projects/components/imported_project_table_row.vue +++ /dev/null @@ -1,59 +0,0 @@ -<script> -import { GlIcon } from '@gitlab/ui'; -import ImportStatus from './import_status.vue'; -import { STATUSES } from '../constants'; - -export default { - name: 'ImportedProjectTableRow', - components: { - ImportStatus, - GlIcon, - }, - props: { - project: { - type: Object, - required: true, - }, - }, - - computed: { - displayFullPath() { - return this.project.importedProject.fullPath.replace(/^\//, ''); - }, - - isFinished() { - return this.project.importStatus === STATUSES.FINISHED; - }, - }, -}; -</script> - -<template> - <tr class="import-row"> - <td> - <a - :href="project.importSource.providerLink" - rel="noreferrer noopener" - target="_blank" - data-testid="providerLink" - >{{ project.importSource.fullName }} - <gl-icon v-if="project.importSource.providerLink" name="external-link" /> - </a> - </td> - <td data-testid="fullPath">{{ displayFullPath }}</td> - <td> - <import-status :status="project.importStatus" /> - </td> - <td> - <a - v-if="isFinished" - class="btn btn-default" - data-testid="goToProject" - :href="project.importedProject.fullPath" - rel="noreferrer noopener" - target="_blank" - >{{ __('Go to project') }} - </a> - </td> - </tr> -</template> diff --git a/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue b/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue deleted file mode 100644 index 3140585ccd7..00000000000 --- a/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue +++ /dev/null @@ -1,32 +0,0 @@ -<script> -import { GlIcon, GlBadge } from '@gitlab/ui'; - -export default { - components: { - GlBadge, - GlIcon, - }, - props: { - repo: { - type: Object, - required: true, - }, - }, -}; -</script> - -<template> - <tr class="import-row"> - <td> - <a :href="repo.importSource.providerLink" rel="noreferrer noopener" target="_blank" - >{{ repo.importSource.fullName }} - <gl-icon v-if="repo.importSource.providerLink" name="external-link" /> - </a> - </td> - <td></td> - <td></td> - <td> - <gl-badge variant="danger">{{ __('Incompatible project') }}</gl-badge> - </td> - </tr> -</template> diff --git a/app/assets/javascripts/import_projects/components/page_query_param_sync.vue b/app/assets/javascripts/import_projects/components/page_query_param_sync.vue deleted file mode 100644 index 5ba3d70f5d0..00000000000 --- a/app/assets/javascripts/import_projects/components/page_query_param_sync.vue +++ /dev/null @@ -1,39 +0,0 @@ -<script> -import { queryToObject, setUrlParams, updateHistory } from '~/lib/utils/url_utility'; - -export default { - props: { - page: { - type: Number, - required: true, - }, - }, - - watch: { - page(newPage) { - updateHistory({ - url: setUrlParams({ - page: newPage === 1 ? null : newPage, - }), - }); - }, - }, - - created() { - window.addEventListener('popstate', this.updatePage); - }, - - beforeDestroy() { - window.removeEventListener('popstate', this.updatePage); - }, - - methods: { - updatePage() { - const page = parseInt(queryToObject(window.location.search).page, 10) || 1; - this.$emit('popstate', page); - }, - }, - - render: () => null, -}; -</script> diff --git a/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue index d8cffc6a7d5..18971313dfe 100644 --- a/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue +++ b/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue @@ -1,9 +1,11 @@ <script> import { mapState, mapGetters, mapActions } from 'vuex'; -import { GlIcon } from '@gitlab/ui'; +import { GlIcon, GlBadge } from '@gitlab/ui'; import Select2Select from '~/vue_shared/components/select2_select.vue'; import { __ } from '~/locale'; import ImportStatus from './import_status.vue'; +import { STATUSES } from '../constants'; +import { isProjectImportable, isIncompatible, getImportStatus } from '../utils'; export default { name: 'ProviderRepoTableRow', @@ -11,6 +13,7 @@ export default { Select2Select, ImportStatus, GlIcon, + GlBadge, }, props: { repo: { @@ -27,6 +30,26 @@ export default { ...mapState(['ciCdOnly']), ...mapGetters(['getImportTarget']), + displayFullPath() { + return this.repo.importedProject.fullPath.replace(/^\//, ''); + }, + + isFinished() { + return this.repo.importedProject?.importStatus === STATUSES.FINISHED; + }, + + isImportNotStarted() { + return isProjectImportable(this.repo); + }, + + isIncompatible() { + return isIncompatible(this.repo); + }, + + importStatus() { + return getImportStatus(this.repo); + }, + importTarget() { return this.getImportTarget(this.repo.importSource.id); }, @@ -85,9 +108,9 @@ export default { <gl-icon v-if="repo.importSource.providerLink" name="external-link" /> </a> </td> - <td class="d-flex flex-wrap flex-lg-nowrap"> - <template v-if="repo.target">{{ repo.target }}</template> - <template v-else> + <td class="d-flex flex-wrap flex-lg-nowrap" data-testid="fullPath"> + <template v-if="repo.importSource.target">{{ repo.importSource.target }}</template> + <template v-else-if="isImportNotStarted"> <select2-select v-model="targetNamespaceSelect" :options="select2Options" /> <span class="px-2 import-slash-divider d-flex justify-content-center align-items-center" >/</span @@ -98,18 +121,31 @@ export default { class="form-control import-project-name-input qa-project-path-field" /> </template> + <template v-else-if="repo.importedProject">{{ displayFullPath }}</template> </td> <td> - <import-status :status="repo.importStatus" /> + <import-status :status="importStatus" /> </td> - <td> + <td data-testid="actions"> + <a + v-if="isFinished" + class="btn btn-default" + :href="repo.importedProject.fullPath" + rel="noreferrer noopener" + target="_blank" + >{{ __('Go to project') }} + </a> <button + v-if="isImportNotStarted" type="button" class="qa-import-button btn btn-default" @click="fetchImport(repo.importSource.id)" > {{ importButtonText }} </button> + <gl-badge v-else-if="isIncompatible" variant="danger">{{ + __('Incompatible project') + }}</gl-badge> </td> </tr> </template> diff --git a/app/assets/javascripts/import_projects/store/actions.js b/app/assets/javascripts/import_projects/store/actions.js index af410f411d8..7b70d290278 100644 --- a/app/assets/javascripts/import_projects/store/actions.js +++ b/app/assets/javascripts/import_projects/store/actions.js @@ -1,11 +1,7 @@ import Visibility from 'visibilityjs'; import * as types from './mutation_types'; import { isProjectImportable } from '../utils'; -import { - convertObjectPropsToCamelCase, - normalizeHeaders, - parseIntPagination, -} from '~/lib/utils/common_utils'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import Poll from '~/lib/utils/poll'; import { visitUrl, objectToQuery } from '~/lib/utils/url_utility'; import { deprecatedCreateFlash as createFlash } from '~/flash'; @@ -54,12 +50,9 @@ const importAll = ({ state, dispatch }) => { ); }; -const fetchReposFactory = ({ reposPath = isRequired(), hasPagination }) => ({ - state, - dispatch, - commit, -}) => { - dispatch('stopJobsPolling'); +const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit }) => { + const nextPage = state.pageInfo.page + 1; + commit(types.SET_PAGE, nextPage); commit(types.REQUEST_REPOS); const { provider, filter } = state; @@ -68,21 +61,16 @@ const fetchReposFactory = ({ reposPath = isRequired(), hasPagination }) => ({ .get( pathWithParams({ path: reposPath, - filter, - page: hasPagination ? state.pageInfo.page.toString() : '', + filter: filter ?? '', + page: nextPage === 1 ? '' : nextPage.toString(), }), ) - .then(({ data, headers }) => { - const normalizedHeaders = normalizeHeaders(headers); - - if ('X-PAGE' in normalizedHeaders) { - commit(types.SET_PAGE_INFO, parseIntPagination(normalizedHeaders)); - } - + .then(({ data }) => { commit(types.RECEIVE_REPOS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })); }) - .then(() => dispatch('fetchJobs')) .catch(e => { + commit(types.SET_PAGE, nextPage - 1); + if (hasRedirectInError(e)) { redirectToUrlInError(e); } else { @@ -136,8 +124,6 @@ const fetchImportFactory = (importPath = isRequired()) => ({ state, commit, gett }; export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, dispatch }) => { - const { filter } = state; - if (eTagPoll) { stopJobsPolling(); clearJobsEtagPoll(); @@ -145,7 +131,7 @@ export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, d eTagPoll = new Poll({ resource: { - fetchJobs: () => axios.get(pathWithParams({ path: jobsPath, filter })), + fetchJobs: () => axios.get(pathWithParams({ path: jobsPath, filter: state.filter })), }, method: 'fetchJobs', successCallback: ({ data }) => @@ -157,7 +143,6 @@ export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, d createFlash(s__('ImportProjects|Update of imported projects with realtime changes failed')); } }, - data: { filter }, }); if (!Visibility.hidden()) { @@ -196,7 +181,7 @@ const setPage = ({ state, commit, dispatch }, page) => { return dispatch('fetchRepos'); }; -export default ({ endpoints = isRequired(), hasPagination }) => ({ +export default ({ endpoints = isRequired() }) => ({ clearJobsEtagPoll, stopJobsPolling, restartJobsPolling, @@ -204,7 +189,7 @@ export default ({ endpoints = isRequired(), hasPagination }) => ({ setImportTarget, importAll, setPage, - fetchRepos: fetchReposFactory({ reposPath: endpoints.reposPath, hasPagination }), + fetchRepos: fetchReposFactory({ reposPath: endpoints.reposPath }), fetchImport: fetchImportFactory(endpoints.importPath), fetchJobs: fetchJobsFactory(endpoints.jobsPath), fetchNamespaces: fetchNamespacesFactory(endpoints.namespacesPath), diff --git a/app/assets/javascripts/import_projects/store/getters.js b/app/assets/javascripts/import_projects/store/getters.js index 7d529c94d7d..b76c52beea2 100644 --- a/app/assets/javascripts/import_projects/store/getters.js +++ b/app/assets/javascripts/import_projects/store/getters.js @@ -1,17 +1,20 @@ import { STATUSES } from '../constants'; +import { isProjectImportable, isIncompatible } from '../utils'; export const isLoading = state => state.isLoadingRepos || state.isLoadingNamespaces; export const isImportingAnyRepo = state => state.repositories.some(repo => - [STATUSES.SCHEDULING, STATUSES.SCHEDULED, STATUSES.STARTED].includes(repo.importStatus), + [STATUSES.SCHEDULING, STATUSES.SCHEDULED, STATUSES.STARTED].includes( + repo.importedProject?.importStatus, + ), ); -export const hasIncompatibleRepos = state => - state.repositories.some(repo => repo.importSource.incompatible); +export const hasIncompatibleRepos = state => state.repositories.some(isIncompatible); -export const hasImportableRepos = state => - state.repositories.some(repo => repo.importStatus === STATUSES.NONE); +export const hasImportableRepos = state => state.repositories.some(isProjectImportable); + +export const importAllCount = state => state.repositories.filter(isProjectImportable).length; export const getImportTarget = state => repoId => { if (state.customImportTargets[repoId]) { diff --git a/app/assets/javascripts/import_projects/store/mutations.js b/app/assets/javascripts/import_projects/store/mutations.js index b3dbef896a6..6999253d4b2 100644 --- a/app/assets/javascripts/import_projects/store/mutations.js +++ b/app/assets/javascripts/import_projects/store/mutations.js @@ -2,46 +2,88 @@ import Vue from 'vue'; import * as types from './mutation_types'; import { STATUSES } from '../constants'; +const makeNewImportedProject = importedProject => ({ + importSource: { + id: importedProject.id, + fullName: importedProject.importSource, + sanitizedName: importedProject.name, + providerLink: importedProject.providerLink, + }, + importedProject, +}); + +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); + const importedProjectShape = factory(project); + + if (existingProject) { + Object.assign(existingProject, importedProjectShape); + } else { + newEntries.push(importedProjectShape); + } + }); + return newEntries; +}; + export default { [types.SET_FILTER](state, filter) { state.filter = filter; + state.repositories = []; + state.pageInfo.page = 0; }, [types.REQUEST_REPOS](state) { state.isLoadingRepos = true; }, - [types.RECEIVE_REPOS_SUCCESS]( - state, - { importedProjects, providerRepos, incompatibleRepos = [] }, - ) { - // Normalizing structure to support legacy backend format - // See https://gitlab.com/gitlab-org/gitlab/-/issues/27370#note_379034091 for details - + [types.RECEIVE_REPOS_SUCCESS](state, repositories) { state.isLoadingRepos = false; - state.repositories = [ - ...importedProjects.map(({ importSource, providerLink, importStatus, ...project }) => ({ - importSource: { - id: `finished-${project.id}`, - fullName: importSource, - sanitizedName: project.name, - providerLink, - }, - importStatus, - importedProject: project, - })), - ...providerRepos.map(project => ({ - importSource: project, - importStatus: STATUSES.NONE, - importedProject: null, - })), - ...incompatibleRepos.map(project => ({ - importSource: { ...project, incompatible: true }, - importStatus: STATUSES.NONE, - importedProject: null, - })), - ]; + if (!Array.isArray(repositories)) { + // Legacy code path, will be removed when all importers will be switched to new pagination format + // https://gitlab.com/gitlab-org/gitlab/-/issues/27370#note_379034091 + + const newImportedProjects = processLegacyEntries({ + newRepositories: repositories.importedProjects, + existingRepositories: state.repositories, + factory: makeNewImportedProject, + }); + + const incompatibleRepos = repositories.incompatibleRepos ?? []; + const newIncompatibleProjects = processLegacyEntries({ + newRepositories: incompatibleRepos, + existingRepositories: state.repositories, + factory: makeNewIncompatibleProject, + }); + + state.repositories = [ + ...newImportedProjects, + ...state.repositories, + ...repositories.providerRepos.map(project => ({ + importSource: project, + importedProject: null, + })), + ...newIncompatibleProjects, + ]; + + if (incompatibleRepos.length === 0 && repositories.providerRepos.length === 0) { + state.pageInfo.page -= 1; + } + + return; + } + + state.repositories = [...state.repositories, ...repositories]; + if (repositories.length === 0) { + state.pageInfo.page -= 1; + } }, [types.RECEIVE_REPOS_ERROR](state) { @@ -50,31 +92,27 @@ export default { [types.REQUEST_IMPORT](state, { repoId, importTarget }) { const existingRepo = state.repositories.find(r => r.importSource.id === repoId); - existingRepo.importStatus = STATUSES.SCHEDULING; existingRepo.importedProject = { + importStatus: STATUSES.SCHEDULING, fullPath: `/${importTarget.targetNamespace}/${importTarget.newName}`, }; }, [types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId }) { - const { importStatus, ...project } = importedProject; - const existingRepo = state.repositories.find(r => r.importSource.id === repoId); - existingRepo.importStatus = importStatus; - existingRepo.importedProject = project; + existingRepo.importedProject = importedProject; }, [types.RECEIVE_IMPORT_ERROR](state, repoId) { const existingRepo = state.repositories.find(r => r.importSource.id === repoId); - existingRepo.importStatus = STATUSES.NONE; existingRepo.importedProject = null; }, [types.RECEIVE_JOBS_SUCCESS](state, updatedProjects) { updatedProjects.forEach(updatedProject => { const repo = state.repositories.find(p => p.importedProject?.id === updatedProject.id); - if (repo) { - repo.importStatus = updatedProject.importStatus; + if (repo?.importedProject) { + repo.importedProject.importStatus = updatedProject.importStatus; } }); }, @@ -105,10 +143,6 @@ export default { } }, - [types.SET_PAGE_INFO](state, pageInfo) { - state.pageInfo = pageInfo; - }, - [types.SET_PAGE](state, page) { state.pageInfo.page = page; }, diff --git a/app/assets/javascripts/import_projects/store/state.js b/app/assets/javascripts/import_projects/store/state.js index 3318181e4af..ecd93561d52 100644 --- a/app/assets/javascripts/import_projects/store/state.js +++ b/app/assets/javascripts/import_projects/store/state.js @@ -8,6 +8,6 @@ export default () => ({ ciCdOnly: false, filter: '', pageInfo: { - page: 1, + page: 0, }, }); diff --git a/app/assets/javascripts/import_projects/utils.js b/app/assets/javascripts/import_projects/utils.js index c2a2d5a607d..695b12cbcba 100644 --- a/app/assets/javascripts/import_projects/utils.js +++ b/app/assets/javascripts/import_projects/utils.js @@ -1,7 +1,13 @@ import { STATUSES } from './constants'; -// Will be expanded in future -// eslint-disable-next-line import/prefer-default-export +export function isIncompatible(project) { + return project.importSource.incompatible; +} + +export function getImportStatus(project) { + return project.importedProject?.importStatus ?? STATUSES.NONE; +} + export function isProjectImportable(project) { - return project.importStatus === STATUSES.NONE && !project.importSource.incompatible; + return !isIncompatible(project) && getImportStatus(project) === STATUSES.NONE; } diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue index 46852e4ddd9..670c42cbdac 100644 --- a/app/assets/javascripts/incidents/components/incidents_list.vue +++ b/app/assets/javascripts/incidents/components/incidents_list.vue @@ -23,6 +23,8 @@ import { s__ } from '~/locale'; import { mergeUrlParams, joinPaths, visitUrl } from '~/lib/utils/url_utility'; import getIncidents from '../graphql/queries/get_incidents.query.graphql'; import getIncidentsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql'; +import SeverityToken from '~/sidebar/components/severity/severity.vue'; +import { INCIDENT_SEVERITY } from '~/sidebar/components/severity/constants'; import { I18N, DEFAULT_PAGE_SIZE, INCIDENT_SEARCH_DELAY, INCIDENT_STATUS_TABS } from '../constants'; const TH_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' }; @@ -45,6 +47,12 @@ export default { statusTabs: INCIDENT_STATUS_TABS, fields: [ { + key: 'severity', + label: s__('IncidentManagement|Severity'), + thClass: `gl-pointer-events-none`, + tdClass, + }, + { key: 'title', label: s__('IncidentManagement|Incident'), thClass: `gl-pointer-events-none gl-w-half`, @@ -82,6 +90,7 @@ export default { PublishedCell: () => import('ee_component/incidents/components/published_cell.vue'), GlBadge, GlEmptyState, + SeverityToken, }, directives: { GlTooltip: GlTooltipDirective, @@ -208,6 +217,31 @@ export default { isEmpty() { return !this.incidents.list?.length; }, + showList() { + return !this.isEmpty || this.errored || this.loading; + }, + activeClosedTabHasNoIncidents() { + const { all, closed } = this.incidentsCount || {}; + const isClosedTabActive = this.statusFilter === this.$options.statusTabs[1].filters; + + return isClosedTabActive && all && !closed; + }, + emptyStateData() { + const { + emptyState: { title, emptyClosedTabTitle, description }, + createIncidentBtnLabel, + } = this.$options.i18n; + + if (this.activeClosedTabHasNoIncidents) { + return { title: emptyClosedTabTitle }; + } + return { + title, + description, + btnLink: this.newIncidentPath, + btnText: createIncidentBtnLabel, + }; + }, }, methods: { onInputChange: debounce(function debounceSearch(input) { @@ -255,6 +289,9 @@ export default { this.sort = `${sortingColumn}_${sortingDirection}`; }, + getSeverity(severity) { + return INCIDENT_SEVERITY[severity]; + }, }, }; </script> @@ -279,7 +316,7 @@ export default { </gl-tabs> <gl-button - v-if="!isEmpty" + v-if="!isEmpty || activeClosedTabHasNoIncidents" class="gl-my-3 gl-mr-5 create-incident-button" data-testid="createIncidentBtn" data-qa-selector="create_incident_button" @@ -307,6 +344,7 @@ export default { {{ s__('IncidentManagement|Incidents') }} </h4> <gl-table + v-if="showList" :items="incidents.list || []" :fields="availableFields" :show-empty="true" @@ -322,6 +360,10 @@ export default { @row-clicked="navigateToIncidentDetails" @sort-changed="fetchSortedData" > + <template #cell(severity)="{ item }"> + <severity-token :severity="getSeverity(item.severity)" /> + </template> + <template #cell(title)="{ item }"> <div :class="{ 'gl-display-flex gl-align-items-center': item.state === 'closed' }"> <div class="gl-max-w-full text-truncate" :title="item.title">{{ item.title }}</div> @@ -379,21 +421,20 @@ export default { <gl-loading-icon size="lg" color="dark" class="mt-3" /> </template> - <template #empty> - <gl-empty-state - v-if="!errored" - :title="$options.i18n.emptyState.title" - :svg-path="emptyListSvgPath" - :description="$options.i18n.emptyState.description" - :primary-button-link="newIncidentPath" - :primary-button-text="$options.i18n.createIncidentBtnLabel" - /> - <span v-else> - {{ $options.i18n.noIncidents }} - </span> + <template v-if="errored" #empty> + {{ $options.i18n.noIncidents }} </template> </gl-table> + <gl-empty-state + v-else + :title="emptyStateData.title" + :svg-path="emptyListSvgPath" + :description="emptyStateData.description" + :primary-button-link="emptyStateData.btnLink" + :primary-button-text="emptyStateData.btnText" + /> + <gl-pagination v-if="showPaginationControls" :value="pagination.currentPage" diff --git a/app/assets/javascripts/incidents/constants.js b/app/assets/javascripts/incidents/constants.js index 02d8172533d..289b36d9848 100644 --- a/app/assets/javascripts/incidents/constants.js +++ b/app/assets/javascripts/incidents/constants.js @@ -9,6 +9,7 @@ export const I18N = { searchPlaceholder: __('Search results…'), emptyState: { title: s__('IncidentManagement|Display your incidents in a dedicated view'), + emptyClosedTabTitle: s__('IncidentManagement|There are no closed incidents'), description: s__( 'IncidentManagement|All alerts promoted to incidents will automatically be displayed within the list. You can also create a new incident using the button below.', ), @@ -34,4 +35,4 @@ export const INCIDENT_STATUS_TABS = [ ]; export const INCIDENT_SEARCH_DELAY = 300; -export const DEFAULT_PAGE_SIZE = 10; +export const DEFAULT_PAGE_SIZE = 20; diff --git a/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql b/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql new file mode 100644 index 00000000000..eb2dde14464 --- /dev/null +++ b/app/assets/javascripts/incidents/graphql/fragments/incident_fields.fragment.graphql @@ -0,0 +1,3 @@ +fragment IncidentFields on Issue { + severity +} diff --git a/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql b/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql index 0f56e8640bd..dab130835e2 100644 --- a/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql +++ b/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql @@ -1,3 +1,5 @@ +#import "ee_else_ce/incidents/graphql/fragments/incident_fields.fragment.graphql" + query getIncidents( $projectPath: ID! $issueTypes: [IssueType!] @@ -39,7 +41,7 @@ query getIncidents( webUrl } } - statusPagePublishedIncident + ...IncidentFields } pageInfo { hasNextPage diff --git a/app/assets/javascripts/incidents_settings/components/alerts_form.vue b/app/assets/javascripts/incidents_settings/components/alerts_form.vue index 5872ac39c96..17a77f650e0 100644 --- a/app/assets/javascripts/incidents_settings/components/alerts_form.vue +++ b/app/assets/javascripts/incidents_settings/components/alerts_form.vue @@ -6,8 +6,8 @@ import { GlIcon, GlFormGroup, GlFormCheckbox, - GlNewDropdown, - GlNewDropdownItem, + GlDropdown, + GlDropdownItem, } from '@gitlab/ui'; import { I18N_ALERT_SETTINGS_FORM, @@ -24,8 +24,8 @@ export default { GlFormGroup, GlIcon, GlFormCheckbox, - GlNewDropdown, - GlNewDropdownItem, + GlDropdown, + GlDropdownItem, }, inject: ['service', 'alertSettings'], data() { @@ -34,6 +34,7 @@ export default { createIssueEnabled: this.alertSettings.createIssue, issueTemplate: this.alertSettings.issueTemplateKey, sendEmailEnabled: this.alertSettings.sendEmail, + autoCloseIncident: this.alertSettings.autoCloseIncident, loading: false, }; }, @@ -49,6 +50,7 @@ export default { create_issue: this.createIssueEnabled, issue_template_key: this.issueTemplate, send_email: this.sendEmailEnabled, + auto_close_incident: this.autoCloseIncident, }; }, }, @@ -99,13 +101,13 @@ export default { <gl-icon name="question" :size="12" /> </gl-link> </label> - <gl-new-dropdown + <gl-dropdown id="alert-integration-settings-issue-template" data-qa-selector="incident_templates_dropdown" :text="issueTemplateHeader" :block="true" > - <gl-new-dropdown-item + <gl-dropdown-item v-for="template in templates" :key="template.key" data-qa-selector="incident_templates_item" @@ -114,8 +116,8 @@ export default { @click="selectIssueTemplate(template.key)" > {{ template.name }} - </gl-new-dropdown-item> - </gl-new-dropdown> + </gl-dropdown-item> + </gl-dropdown> </gl-form-group> <gl-form-group class="gl-pl-0 gl-mb-5"> @@ -123,6 +125,11 @@ export default { <span>{{ $options.i18n.sendEmail.label }}</span> </gl-form-checkbox> </gl-form-group> + <gl-form-group class="gl-pl-0 gl-mb-5"> + <gl-form-checkbox v-model="autoCloseIncident"> + <span>{{ $options.i18n.autoCloseIncidents.label }}</span> + </gl-form-checkbox> + </gl-form-group> <div class="gl-display-flex gl-justify-content-end"> <gl-button ref="submitBtn" diff --git a/app/assets/javascripts/incidents_settings/constants.js b/app/assets/javascripts/incidents_settings/constants.js index 77f7ee2c4a3..42f1f645d16 100644 --- a/app/assets/javascripts/incidents_settings/constants.js +++ b/app/assets/javascripts/incidents_settings/constants.js @@ -42,6 +42,9 @@ export const I18N_ALERT_SETTINGS_FORM = { sendEmail: { label: __('Send a separate email notification to Developers.'), }, + autoCloseIncidents: { + label: __('Automatically close incident issues when the associated Prometheus alert resolves.'), + }, }; export const NO_ISSUE_TEMPLATE_SELECTED = { key: '', name: __('No template selected') }; diff --git a/app/assets/javascripts/incidents_settings/index.js b/app/assets/javascripts/incidents_settings/index.js index 80e7d07feca..ad875d49768 100644 --- a/app/assets/javascripts/incidents_settings/index.js +++ b/app/assets/javascripts/incidents_settings/index.js @@ -20,6 +20,7 @@ export default () => { pagerdutyActive, pagerdutyWebhookUrl, pagerdutyResetKeyPath, + autoCloseIncident, }, } = el; @@ -33,6 +34,7 @@ export default () => { createIssue: parseBoolean(createIssue), issueTemplateKey, sendEmail: parseBoolean(sendEmail), + autoCloseIncident: parseBoolean(autoCloseIncident), }, pagerDutySettings: { active: parseBoolean(pagerdutyActive), diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js index e708e5d0978..c0dc6ce07b1 100644 --- a/app/assets/javascripts/init_changes_dropdown.js +++ b/app/assets/javascripts/init_changes_dropdown.js @@ -1,10 +1,11 @@ import $ from 'jquery'; import { stickyMonitor } from './lib/utils/sticky'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default stickyTop => { stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop); - $('.js-diff-stats-dropdown').glDropdown({ + initDeprecatedJQueryDropdown($('.js-diff-stats-dropdown'), { filterable: true, remoteFilter: false, }); diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js index e61b37a2d1f..528d5d8072f 100644 --- a/app/assets/javascripts/init_issuable_sidebar.js +++ b/app/assets/javascripts/init_issuable_sidebar.js @@ -4,8 +4,8 @@ import MilestoneSelect from './milestone_select'; import LabelsSelect from './labels_select'; import IssuableContext from './issuable_context'; import Sidebar from './right_sidebar'; - import DueDateSelectors from './due_date_select'; +import { mountSidebarLabels } from '~/sidebar/mount_sidebar'; export default () => { const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML); @@ -17,4 +17,6 @@ export default () => { new IssuableContext(sidebarOptions.currentUser); new DueDateSelectors(); Sidebar.initialize(); + + mountSidebarLabels(); }; diff --git a/app/assets/javascripts/integrations/edit/components/active_toggle.vue b/app/assets/javascripts/integrations/edit/components/active_checkbox.vue index e6a96600539..6698984d02f 100644 --- a/app/assets/javascripts/integrations/edit/components/active_toggle.vue +++ b/app/assets/javascripts/integrations/edit/components/active_checkbox.vue @@ -1,36 +1,31 @@ <script> import { mapGetters } from 'vuex'; -import { GlFormGroup, GlToggle } from '@gitlab/ui'; +import { GlFormGroup, GlFormCheckbox } from '@gitlab/ui'; import eventHub from '../event_hub'; export default { - name: 'ActiveToggle', + name: 'ActiveCheckbox', components: { GlFormGroup, - GlToggle, - }, - props: { - initialActivated: { - type: Boolean, - required: true, - }, + GlFormCheckbox, }, data() { return { - activated: this.initialActivated, + activated: false, }; }, computed: { - ...mapGetters(['isInheriting']), + ...mapGetters(['isInheriting', 'propsSource']), }, mounted() { + this.activated = this.propsSource.initialActivated; // Initialize view this.$nextTick(() => { - this.onToggle(this.activated); + this.onChange(this.activated); }); }, methods: { - onToggle(e) { + onChange(e) { eventHub.$emit('toggle', e); }, }, @@ -39,12 +34,15 @@ export default { <template> <gl-form-group :label="__('Enable integration')" label-for="service[active]"> - <gl-toggle + <input name="service[active]" type="hidden" :value="activated || false" /> + <gl-form-checkbox v-model="activated" name="service[active]" class="gl-display-block gl-line-height-0" :disabled="isInheriting" - @change="onToggle" - /> + @change="onChange" + > + {{ __('Active') }} + </gl-form-checkbox> </gl-form-group> </template> diff --git a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue index 090381b8da4..9dde1ed1055 100644 --- a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue +++ b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { mapGetters } from 'vuex'; import { capitalize, lowerCase, isEmpty } from 'lodash'; import { GlFormGroup, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormTextarea } from '@gitlab/ui'; diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue index 5088664c3bd..0460ed6791e 100644 --- a/app/assets/javascripts/integrations/edit/components/integration_form.vue +++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue @@ -1,9 +1,11 @@ <script> import { mapState, mapActions, mapGetters } from 'vuex'; +import { GlButton } from '@gitlab/ui'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import eventHub from '../event_hub'; import OverrideDropdown from './override_dropdown.vue'; -import ActiveToggle from './active_toggle.vue'; +import ActiveCheckbox from './active_checkbox.vue'; import JiraTriggerFields from './jira_trigger_fields.vue'; import JiraIssuesFields from './jira_issues_fields.vue'; import TriggerFields from './trigger_fields.vue'; @@ -13,16 +15,20 @@ export default { name: 'IntegrationForm', components: { OverrideDropdown, - ActiveToggle, + ActiveCheckbox, JiraTriggerFields, JiraIssuesFields, TriggerFields, DynamicField, + GlButton, }, mixins: [glFeatureFlagsMixin()], computed: { - ...mapGetters(['currentKey', 'propsSource']), - ...mapState(['adminState', 'override']), + ...mapGetters(['currentKey', 'propsSource', 'isSavingOrTesting']), + ...mapState(['defaultState', 'override', 'isSaving', 'isTesting']), + isEditable() { + return this.propsSource.editable; + }, isJira() { return this.propsSource.type === 'jira'; }, @@ -31,7 +37,15 @@ export default { }, }, methods: { - ...mapActions(['setOverride']), + ...mapActions(['setOverride', 'setIsSaving', 'setIsTesting']), + onSaveClick() { + this.setIsSaving(true); + eventHub.$emit('saveIntegration'); + }, + onTestClick() { + this.setIsTesting(true); + eventHub.$emit('testIntegration'); + }, }, }; </script> @@ -39,16 +53,13 @@ export default { <template> <div> <override-dropdown - v-if="adminState !== null" - :inherit-from-id="adminState.id" + v-if="defaultState !== null" + :inherit-from-id="defaultState.id" :override="override" + :learn-more-path="propsSource.learnMorePath" @change="setOverride" /> - <active-toggle - v-if="propsSource.showActive" - :key="`${currentKey}-active-toggle`" - v-bind="propsSource.activeToggleProps" - /> + <active-checkbox v-if="propsSource.showActive" :key="`${currentKey}-active-checkbox`" /> <jira-trigger-fields v-if="isJira" :key="`${currentKey}-jira-trigger-fields`" @@ -70,5 +81,29 @@ export default { :key="`${currentKey}-jira-issues-fields`" v-bind="propsSource.jiraIssuesProps" /> + <div v-if="isEditable" class="footer-block row-content-block"> + <gl-button + category="primary" + variant="success" + type="submit" + :loading="isSaving" + :disabled="isSavingOrTesting" + data-qa-selector="save_changes_button" + @click.prevent="onSaveClick" + > + {{ __('Save changes') }} + </gl-button> + <gl-button + v-if="propsSource.canTest" + :loading="isTesting" + :disabled="isSavingOrTesting" + :href="propsSource.testPath" + @click.prevent="onTestClick" + > + {{ __('Test settings') }} + </gl-button> + + <gl-button class="btn-cancel" :href="propsSource.cancelPath">{{ __('Cancel') }}</gl-button> + </div> </div> </template> diff --git a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue index 5a1f86718b0..1baa2b440b0 100644 --- a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue +++ b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue @@ -37,6 +37,11 @@ export default { required: false, default: null, }, + gitlabIssuesEnabled: { + type: Boolean, + required: false, + default: true, + }, upgradePlanPath: { type: String, required: false, @@ -133,7 +138,7 @@ export default { :disabled="!enableJiraIssues" /> </gl-form-group> - <p> + <p v-if="gitlabIssuesEnabled"> <gl-sprintf :message=" s__( diff --git a/app/assets/javascripts/integrations/edit/components/override_dropdown.vue b/app/assets/javascripts/integrations/edit/components/override_dropdown.vue index accfc26974c..c31dada8d2f 100644 --- a/app/assets/javascripts/integrations/edit/components/override_dropdown.vue +++ b/app/assets/javascripts/integrations/edit/components/override_dropdown.vue @@ -1,6 +1,8 @@ <script> -import { GlNewDropdown, GlNewDropdownItem } from '@gitlab/ui'; +import { mapState } from 'vuex'; +import { GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui'; import { s__ } from '~/locale'; +import { defaultIntegrationLevel, overrideDropdownDescriptions } from '../constants'; const dropdownOptions = [ { @@ -17,14 +19,20 @@ export default { dropdownOptions, name: 'OverrideDropdown', components: { - GlNewDropdown, - GlNewDropdownItem, + GlDropdown, + GlDropdownItem, + GlLink, }, props: { inheritFromId: { type: Number, required: true, }, + learnMorePath: { + type: String, + required: false, + default: null, + }, override: { type: Boolean, required: true, @@ -35,6 +43,16 @@ export default { selected: dropdownOptions.find(x => x.value === this.override), }; }, + computed: { + ...mapState(['defaultState']), + description() { + const level = this.defaultState.integrationLevel; + + return ( + overrideDropdownDescriptions[level] || overrideDropdownDescriptions[defaultIntegrationLevel] + ); + }, + }, methods: { onClick(option) { this.selected = option; @@ -48,16 +66,21 @@ export default { <div class="gl-display-flex gl-justify-content-space-between gl-align-items-baseline gl-py-4 gl-mt-5 gl-mb-6 gl-border-t-1 gl-border-t-solid gl-border-b-1 gl-border-b-solid gl-border-gray-100" > - <span>{{ s__('Integrations|Default settings are inherited from the instance level.') }}</span> + <span + >{{ description }} + <gl-link v-if="learnMorePath" :href="learnMorePath" target="_blank">{{ + __('Learn more') + }}</gl-link> + </span> <input name="service[inherit_from_id]" :value="override ? '' : inheritFromId" type="hidden" /> - <gl-new-dropdown :text="selected.text"> - <gl-new-dropdown-item + <gl-dropdown :text="selected.text"> + <gl-dropdown-item v-for="option in $options.dropdownOptions" :key="option.value" @click="onClick(option)" > {{ option.text }} - </gl-new-dropdown-item> - </gl-new-dropdown> + </gl-dropdown-item> + </gl-dropdown> </div> </template> diff --git a/app/assets/javascripts/integrations/edit/constants.js b/app/assets/javascripts/integrations/edit/constants.js new file mode 100644 index 00000000000..b74ae209eb7 --- /dev/null +++ b/app/assets/javascripts/integrations/edit/constants.js @@ -0,0 +1,17 @@ +import { s__ } from '~/locale'; + +export const integrationLevels = { + GROUP: 'group', + INSTANCE: 'instance', +}; + +export const defaultIntegrationLevel = integrationLevels.INSTANCE; + +export const overrideDropdownDescriptions = { + [integrationLevels.GROUP]: s__( + 'Integrations|Default settings are inherited from the group level.', + ), + [integrationLevels.INSTANCE]: s__( + 'Integrations|Default settings are inherited from the instance level.', + ), +}; diff --git a/app/assets/javascripts/integrations/edit/index.js b/app/assets/javascripts/integrations/edit/index.js index ea5463832ce..248ee62d43a 100644 --- a/app/assets/javascripts/integrations/edit/index.js +++ b/app/assets/javascripts/integrations/edit/index.js @@ -19,27 +19,36 @@ function parseDatasetToProps(data) { projectKey, upgradePlanPath, editProjectPath, + learnMorePath, triggerEvents, fields, inheritFromId, + integrationLevel, + cancelPath, + testPath, ...booleanAttributes } = data; const { showActive, activated, + editable, + canTest, commitEvents, mergeRequestEvents, enableComments, showJiraIssuesIntegration, enableJiraIssues, + gitlabIssuesEnabled, } = parseBooleanInData(booleanAttributes); return { - activeToggleProps: { - initialActivated: activated, - }, + initialActivated: activated, showActive, type, + cancelPath, + editable, + canTest, + testPath, triggerFieldsProps: { initialTriggerCommit: commitEvents, initialTriggerMergeRequest: mergeRequestEvents, @@ -50,17 +59,20 @@ function parseDatasetToProps(data) { showJiraIssuesIntegration, initialEnableJiraIssues: enableJiraIssues, initialProjectKey: projectKey, + gitlabIssuesEnabled, upgradePlanPath, editProjectPath, }, + learnMorePath, triggerEvents: JSON.parse(triggerEvents), fields: JSON.parse(fields), inheritFromId: parseInt(inheritFromId, 10), + integrationLevel, id: parseInt(id, 10), }; } -export default (el, adminEl) => { +export default (el, defaultEl) => { if (!el) { return null; } @@ -68,12 +80,12 @@ export default (el, adminEl) => { const props = parseDatasetToProps(el.dataset); const initialState = { - adminState: null, + defaultState: null, customState: props, }; - if (adminEl) { - initialState.adminState = Object.freeze(parseDatasetToProps(adminEl.dataset)); + if (defaultEl) { + initialState.defaultState = Object.freeze(parseDatasetToProps(defaultEl.dataset)); } return new Vue({ diff --git a/app/assets/javascripts/integrations/edit/store/actions.js b/app/assets/javascripts/integrations/edit/store/actions.js index 3decdaab55d..199c9074ead 100644 --- a/app/assets/javascripts/integrations/edit/store/actions.js +++ b/app/assets/javascripts/integrations/edit/store/actions.js @@ -1,4 +1,5 @@ import * as types from './mutation_types'; -// eslint-disable-next-line import/prefer-default-export export const setOverride = ({ commit }, override) => commit(types.SET_OVERRIDE, override); +export const setIsSaving = ({ commit }, isSaving) => commit(types.SET_IS_SAVING, isSaving); +export const setIsTesting = ({ commit }, isTesting) => commit(types.SET_IS_TESTING, isTesting); diff --git a/app/assets/javascripts/integrations/edit/store/getters.js b/app/assets/javascripts/integrations/edit/store/getters.js index b68bd668980..4ee5f11855c 100644 --- a/app/assets/javascripts/integrations/edit/store/getters.js +++ b/app/assets/javascripts/integrations/edit/store/getters.js @@ -1,6 +1,8 @@ -export const isInheriting = state => (state.adminState === null ? false : !state.override); +export const isInheriting = state => (state.defaultState === null ? false : !state.override); + +export const isSavingOrTesting = state => state.isSaving || state.isTesting; export const propsSource = (state, getters) => - getters.isInheriting ? state.adminState : state.customState; + getters.isInheriting ? state.defaultState : state.customState; export const currentKey = (state, getters) => (getters.isInheriting ? 'admin' : 'custom'); diff --git a/app/assets/javascripts/integrations/edit/store/index.js b/app/assets/javascripts/integrations/edit/store/index.js index eea5e48780d..a8375f345c6 100644 --- a/app/assets/javascripts/integrations/edit/store/index.js +++ b/app/assets/javascripts/integrations/edit/store/index.js @@ -7,7 +7,6 @@ import createState from './state'; Vue.use(Vuex); -// eslint-disable-next-line import/prefer-default-export export const createStore = (initialState = {}) => new Vuex.Store({ actions, diff --git a/app/assets/javascripts/integrations/edit/store/mutation_types.js b/app/assets/javascripts/integrations/edit/store/mutation_types.js index 274afe3fb49..0dae8ea079e 100644 --- a/app/assets/javascripts/integrations/edit/store/mutation_types.js +++ b/app/assets/javascripts/integrations/edit/store/mutation_types.js @@ -1,2 +1,3 @@ -// eslint-disable-next-line import/prefer-default-export export const SET_OVERRIDE = 'SET_OVERRIDE'; +export const SET_IS_SAVING = 'SET_IS_SAVING'; +export const SET_IS_TESTING = 'SET_IS_TESTING'; diff --git a/app/assets/javascripts/integrations/edit/store/mutations.js b/app/assets/javascripts/integrations/edit/store/mutations.js index 8757d415197..8ac3c476f9e 100644 --- a/app/assets/javascripts/integrations/edit/store/mutations.js +++ b/app/assets/javascripts/integrations/edit/store/mutations.js @@ -4,4 +4,10 @@ export default { [types.SET_OVERRIDE](state, override) { state.override = override; }, + [types.SET_IS_SAVING](state, isSaving) { + state.isSaving = isSaving; + }, + [types.SET_IS_TESTING](state, isTesting) { + state.isTesting = isTesting; + }, }; diff --git a/app/assets/javascripts/integrations/edit/store/state.js b/app/assets/javascripts/integrations/edit/store/state.js index 95c1a2be500..a9ecee6c539 100644 --- a/app/assets/javascripts/integrations/edit/store/state.js +++ b/app/assets/javascripts/integrations/edit/store/state.js @@ -1,9 +1,11 @@ -export default ({ adminState = null, customState = {} } = {}) => { - const override = adminState !== null ? adminState.id !== customState.inheritFromId : false; +export default ({ defaultState = null, customState = {} } = {}) => { + const override = defaultState !== null ? defaultState.id !== customState.inheritFromId : false; return { override, - adminState, + defaultState, customState, + isSaving: false, + isTesting: false, }; }; diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js index 1135065b06c..1d0814125e6 100644 --- a/app/assets/javascripts/integrations/integration_settings_form.js +++ b/app/assets/javascripts/integrations/integration_settings_form.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import axios from '../lib/utils/axios_utils'; -import { deprecatedCreateFlash as flash } from '../flash'; -import { __ } from '~/locale'; +import { __, s__ } from '~/locale'; +import toast from '~/vue_shared/plugins/global_toast'; import initForm from './edit'; import eventHub from './edit/event_hub'; @@ -10,65 +10,63 @@ export default class IntegrationSettingsForm { this.$form = $(formSelector); this.formActive = false; + this.vue = null; + // Form Metadata - this.canTestService = this.$form.data('canTest'); this.testEndPoint = this.$form.data('testUrl'); - - // Form Child Elements - this.$submitBtn = this.$form.find('button[type="submit"]'); - this.$submitBtnLoader = this.$submitBtn.find('.js-btn-spinner'); - this.$submitBtnLabel = this.$submitBtn.find('.js-btn-label'); } init() { // Init Vue component - initForm( + this.vue = initForm( document.querySelector('.js-vue-integration-settings'), - document.querySelector('.js-vue-admin-integration-settings'), + document.querySelector('.js-vue-default-integration-settings'), ); eventHub.$on('toggle', active => { this.formActive = active; - this.handleServiceToggle(); + this.toggleServiceState(); + }); + eventHub.$on('testIntegration', () => { + this.testIntegration(); + }); + eventHub.$on('saveIntegration', () => { + this.saveIntegration(); }); - - // Bind Event Listeners - this.$submitBtn.on('click', e => this.handleSettingsSave(e)); } - handleSettingsSave(e) { - // Check if Service is marked active, as if not marked active, - // We can skip testing it and directly go ahead to allow form to - // be submitted - if (!this.formActive) { - return; + saveIntegration() { + // Service was marked active so now we check; + // 1) If form contents are valid + // 2) If this service can be saved + // If both conditions are true, we override form submission + // and save the service using provided configuration. + if (this.$form.get(0).checkValidity()) { + this.$form.submit(); + } else { + eventHub.$emit('validateForm'); + this.vue.$store.dispatch('setIsSaving', false); } + } + testIntegration() { // Service was marked active so now we check; // 1) If form contents are valid // 2) If this service can be tested // If both conditions are true, we override form submission // and test the service using provided configuration. if (this.$form.get(0).checkValidity()) { - if (this.canTestService) { - e.preventDefault(); - // eslint-disable-next-line no-jquery/no-serialize - this.testSettings(this.$form.serialize()); - } + // eslint-disable-next-line no-jquery/no-serialize + this.testSettings(this.$form.serialize()); } else { - e.preventDefault(); eventHub.$emit('validateForm'); + this.vue.$store.dispatch('setIsTesting', false); } } - handleServiceToggle() { - this.toggleServiceState(); - } - /** * Change Form's validation enforcement based on service status (active/inactive) */ toggleServiceState() { - this.toggleSubmitBtnLabel(); if (this.formActive) { this.$form.removeAttr('novalidate'); } else if (!this.$form.attr('novalidate')) { @@ -77,67 +75,23 @@ export default class IntegrationSettingsForm { } /** - * Toggle Submit button label based on Integration status and ability to test service - */ - toggleSubmitBtnLabel() { - let btnLabel = __('Save changes'); - - if (this.formActive && this.canTestService) { - btnLabel = __('Test settings and save changes'); - } - - this.$submitBtnLabel.text(btnLabel); - } - - /** - * Toggle Submit button state based on provided boolean value of `saveTestActive` - * When enabled, it does two things, and reverts back when disabled - * - * 1. It shows load spinner on submit button - * 2. Makes submit button disabled - */ - toggleSubmitBtnState(saveTestActive) { - if (saveTestActive) { - this.$submitBtn.disable(); - this.$submitBtnLoader.removeClass('hidden'); - } else { - this.$submitBtn.enable(); - this.$submitBtnLoader.addClass('hidden'); - } - } - - /** * Test Integration config */ testSettings(formData) { - this.toggleSubmitBtnState(true); - return axios .put(this.testEndPoint, formData) .then(({ data }) => { if (data.error) { - let flashActions; - - if (data.test_failed) { - flashActions = { - title: __('Save anyway'), - clickHandler: e => { - e.preventDefault(); - this.$form.submit(); - }, - }; - } - - flash(`${data.message} ${data.service_response}`, 'alert', document, flashActions); + toast(`${data.message} ${data.service_response}`); } else { - this.$form.submit(); + toast(s__('Integrations|Connection successful.')); } - - this.toggleSubmitBtnState(false); }) .catch(() => { - flash(__('Something went wrong on our end.')); - this.toggleSubmitBtnState(false); + toast(__('Something went wrong on our end.')); + }) + .finally(() => { + this.vue.$store.dispatch('setIsTesting', false); }); } } diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index 85c2a370ff3..7d9cefbe66a 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -7,7 +7,7 @@ import MilestoneSelect from './milestone_select'; import issueStatusSelect from './issue_status_select'; import subscriptionSelect from './subscription_select'; import LabelsSelect from './labels_select'; -import issueableEventHub from './issuables_list/eventhub'; +import issueableEventHub from './issues_list/eventhub'; const HIDDEN_CLASS = 'hidden'; const DISABLED_CONTENT_CLASS = 'disabled-content'; diff --git a/app/assets/javascripts/issuable_create/components/issuable_create_root.vue b/app/assets/javascripts/issuable_create/components/issuable_create_root.vue new file mode 100644 index 00000000000..1ef42976032 --- /dev/null +++ b/app/assets/javascripts/issuable_create/components/issuable_create_root.vue @@ -0,0 +1,44 @@ +<script> +import IssuableForm from './issuable_form.vue'; + +export default { + components: { + IssuableForm, + }, + props: { + descriptionPreviewPath: { + type: String, + required: true, + }, + descriptionHelpPath: { + type: String, + required: true, + }, + labelsFetchPath: { + type: String, + required: true, + }, + labelsManagePath: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <div class="issuable-create-container"> + <slot name="title"></slot> + <hr /> + <issuable-form + :description-preview-path="descriptionPreviewPath" + :description-help-path="descriptionHelpPath" + :labels-fetch-path="labelsFetchPath" + :labels-manage-path="labelsManagePath" + > + <template #actions="issuableMeta"> + <slot name="actions" v-bind="issuableMeta"></slot> + </template> + </issuable-form> + </div> +</template> diff --git a/app/assets/javascripts/issuable_create/components/issuable_form.vue b/app/assets/javascripts/issuable_create/components/issuable_form.vue new file mode 100644 index 00000000000..17e51b3dbac --- /dev/null +++ b/app/assets/javascripts/issuable_create/components/issuable_form.vue @@ -0,0 +1,127 @@ +<script> +import { GlForm, GlFormInput } from '@gitlab/ui'; +import MarkdownField from '~/vue_shared/components/markdown/field.vue'; +import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue'; + +import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants'; + +export default { + LabelSelectVariant: DropdownVariant, + components: { + GlForm, + GlFormInput, + MarkdownField, + LabelsSelect, + }, + props: { + descriptionPreviewPath: { + type: String, + required: true, + }, + descriptionHelpPath: { + type: String, + required: true, + }, + labelsFetchPath: { + type: String, + required: true, + }, + labelsManagePath: { + type: String, + required: true, + }, + }, + data() { + return { + issuableTitle: '', + issuableDescription: '', + selectedLabels: [], + }; + }, + methods: { + handleUpdateSelectedLabels(labels) { + if (labels.length) { + this.selectedLabels = labels; + } + }, + }, +}; +</script> + +<template> + <gl-form class="common-note-form gfm-form" @submit.stop.prevent> + <div data-testid="issuable-title" class="form-group row"> + <label for="issuable-title" class="col-form-label col-sm-2">{{ __('Title') }}</label> + <div class="col-sm-10"> + <gl-form-input + id="issuable-title" + v-model="issuableTitle" + :autofocus="true" + :placeholder="__('Title')" + /> + </div> + </div> + <div data-testid="issuable-description" class="form-group row"> + <label for="issuable-description" class="col-form-label col-sm-2">{{ + __('Description') + }}</label> + <div class="col-sm-10"> + <markdown-field + :markdown-preview-path="descriptionPreviewPath" + :markdown-docs-path="descriptionHelpPath" + :add-spacing-classes="false" + :show-suggest-popover="true" + > + <textarea + id="issuable-description" + ref="textarea" + slot="textarea" + v-model="issuableDescription" + dir="auto" + class="note-textarea qa-issuable-form-description rspec-issuable-form-description js-gfm-input js-autosize markdown-area" + :aria-label="__('Description')" + :placeholder="__('Write a comment or drag your files here…')" + ></textarea> + </markdown-field> + </div> + </div> + <div class="row"> + <div class="col-lg-6"> + <div data-testid="issuable-labels" class="form-group row"> + <label for="issuable-labels" class="col-form-label col-md-2 col-lg-4">{{ + __('Labels') + }}</label> + <div class="col-md-8 col-sm-10"> + <div class="issuable-form-select-holder"> + <labels-select + :allow-label-edit="true" + :allow-label-create="true" + :allow-multiselect="true" + :allow-scoped-labels="true" + :labels-fetch-path="labelsFetchPath" + :labels-manage-path="labelsManagePath" + :selected-labels="selectedLabels" + :labels-list-title="__('Select label')" + :footer-create-label-title="__('Create project label')" + :footer-manage-label-title="__('Manage project labels')" + :variant="$options.LabelSelectVariant.Embedded" + @updateSelectedLabels="handleUpdateSelectedLabels" + /> + </div> + </div> + </div> + </div> + </div> + <div + data-testid="issuable-create-actions" + class="footer-block row-content-block gl-display-flex" + > + <slot + name="actions" + :issuable-title="issuableTitle" + :issuable-description="issuableDescription" + :selected-labels="selectedLabels" + ></slot> + </div> + </gl-form> +</template> diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 2dcf5e6a0d6..ed34e2f5623 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -66,6 +66,7 @@ export default class IssuableForm { gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources, ).setup(); this.usersSelect = new UsersSelect(); + this.reviewersSelect = new UsersSelect(undefined, '.js-reviewer-search'); this.zenMode = new ZenMode(); this.titleField = this.form.find('input[name*="[title]"]'); diff --git a/app/assets/javascripts/issuable_list/components/issuable_item.vue b/app/assets/javascripts/issuable_list/components/issuable_item.vue new file mode 100644 index 00000000000..d8cb1ab07cd --- /dev/null +++ b/app/assets/javascripts/issuable_list/components/issuable_item.vue @@ -0,0 +1,140 @@ +<script> +import { GlLink, GlLabel, GlTooltipDirective } from '@gitlab/ui'; + +import { __, sprintf } from '~/locale'; +import { getTimeago } from '~/lib/utils/datetime_utility'; +import { isScopedLabel } from '~/lib/utils/common_utils'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; + +export default { + components: { + GlLink, + GlLabel, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + mixins: [timeagoMixin], + props: { + issuableSymbol: { + type: String, + required: true, + }, + issuable: { + type: Object, + required: true, + }, + }, + computed: { + author() { + return this.issuable.author; + }, + authorId() { + const id = parseInt(this.author.id, 10); + + if (Number.isNaN(id)) { + return this.author.id.includes('gid') + ? this.author.id.split('gid://gitlab/User/').pop() + : ''; + } + + return id; + }, + labels() { + return this.issuable.labels?.nodes || this.issuable.labels || []; + }, + createdAt() { + return sprintf(__('created %{timeAgo}'), { + timeAgo: getTimeago().format(this.issuable.createdAt), + }); + }, + updatedAt() { + return sprintf(__('updated %{timeAgo}'), { + timeAgo: getTimeago().format(this.issuable.updatedAt), + }); + }, + }, + methods: { + scopedLabel(label) { + return isScopedLabel(label); + }, + /** + * This is needed as an independent method since + * when user changes current page, `$refs.authorLink` + * will be null until next page results are loaded & rendered. + */ + getAuthorPopoverTarget() { + if (this.$refs.authorLink) { + return this.$refs.authorLink.$el; + } + return ''; + }, + }, +}; +</script> + +<template> + <li class="issue"> + <div class="issue-box"> + <div class="issuable-info-container"> + <div class="issuable-main-info"> + <div data-testid="issuable-title" class="issue-title title"> + <span class="issue-title-text" dir="auto"> + <gl-link :href="issuable.webUrl">{{ issuable.title }}</gl-link> + </span> + </div> + <div class="issuable-info"> + <span data-testid="issuable-reference" class="issuable-reference" + >{{ issuableSymbol }}{{ issuable.iid }}</span + > + <span class="issuable-authored d-none d-sm-inline-block"> + · + <span + v-gl-tooltip:tooltipcontainer.bottom + data-testid="issuable-created-at" + :title="tooltipTitle(issuable.createdAt)" + >{{ createdAt }}</span + > + {{ __('by') }} + <gl-link + :data-user-id="authorId" + :data-username="author.username" + :data-name="author.name" + :data-avatar-url="author.avatarUrl" + :href="author.webUrl" + data-testid="issuable-author" + class="author-link js-user-link" + > + <span class="author">{{ author.name }}</span> + </gl-link> + </span> + + <gl-label + v-for="(label, index) in labels" + :key="index" + :background-color="label.color" + :title="label.title" + :description="label.description" + :scoped="scopedLabel(label)" + :class="{ 'gl-ml-2': index }" + size="sm" + /> + </div> + </div> + <div class="issuable-meta"> + <div + data-testid="issuable-updated-at" + class="float-right issuable-updated-at d-none d-sm-inline-block" + > + <span + v-gl-tooltip:tooltipcontainer.bottom + :title="tooltipTitle(issuable.updatedAt)" + class="issuable-updated-at" + >{{ updatedAt }}</span + > + </div> + </div> + </div> + </div> + </li> +</template> diff --git a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue new file mode 100644 index 00000000000..7535203dea1 --- /dev/null +++ b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue @@ -0,0 +1,153 @@ +<script> +import { GlLoadingIcon, GlPagination } from '@gitlab/ui'; + +import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; + +import IssuableTabs from './issuable_tabs.vue'; +import IssuableItem from './issuable_item.vue'; + +export default { + components: { + GlLoadingIcon, + IssuableTabs, + FilteredSearchBar, + IssuableItem, + GlPagination, + }, + props: { + namespace: { + type: String, + required: true, + }, + recentSearchesStorageKey: { + type: String, + required: true, + }, + searchInputPlaceholder: { + type: String, + required: true, + }, + searchTokens: { + type: Array, + required: true, + }, + sortOptions: { + type: Array, + required: true, + }, + initialFilterValue: { + type: Array, + required: false, + default: () => [], + }, + initialSortBy: { + type: String, + required: false, + default: 'created_desc', + }, + issuables: { + type: Array, + required: true, + }, + tabs: { + type: Array, + required: true, + }, + tabCounts: { + type: Object, + required: true, + }, + currentTab: { + type: String, + required: true, + }, + issuableSymbol: { + type: String, + required: false, + default: '#', + }, + issuablesLoading: { + type: Boolean, + required: false, + default: false, + }, + showPaginationControls: { + type: Boolean, + required: false, + default: false, + }, + defaultPageSize: { + type: Number, + required: false, + default: 20, + }, + currentPage: { + type: Number, + required: false, + default: 1, + }, + previousPage: { + type: Number, + required: false, + default: 0, + }, + nextPage: { + type: Number, + required: false, + default: 2, + }, + }, +}; +</script> + +<template> + <div class="issuable-list-container"> + <issuable-tabs + :tabs="tabs" + :tab-counts="tabCounts" + :current-tab="currentTab" + @click="$emit('click-tab', $event)" + > + <template #nav-actions> + <slot name="nav-actions"></slot> + </template> + </issuable-tabs> + <filtered-search-bar + :namespace="namespace" + :recent-searches-storage-key="recentSearchesStorageKey" + :search-input-placeholder="searchInputPlaceholder" + :tokens="searchTokens" + :sort-options="sortOptions" + :initial-filter-value="initialFilterValue" + :initial-sort-by="initialSortBy" + class="gl-flex-grow-1 row-content-block" + @onFilter="$emit('filter', $event)" + @onSort="$emit('sort', $event)" + /> + <div class="issuables-holder"> + <gl-loading-icon v-if="issuablesLoading" size="md" class="gl-mt-5" /> + <ul + v-if="!issuablesLoading && issuables.length" + class="content-list issuable-list issues-list" + > + <issuable-item + v-for="issuable in issuables" + :key="issuable.id" + :issuable-symbol="issuableSymbol" + :issuable="issuable" + /> + </ul> + <slot v-if="!issuablesLoading && !issuables.length" name="empty-state"></slot> + <gl-pagination + v-if="showPaginationControls" + :per-page="defaultPageSize" + :value="currentPage" + :prev-page="previousPage" + :next-page="nextPage" + align="center" + class="gl-pagination gl-mt-3" + @input="$emit('page-change', $event)" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/issuable_list/components/issuable_tabs.vue b/app/assets/javascripts/issuable_list/components/issuable_tabs.vue new file mode 100644 index 00000000000..df544ce69e7 --- /dev/null +++ b/app/assets/javascripts/issuable_list/components/issuable_tabs.vue @@ -0,0 +1,53 @@ +<script> +import { GlTabs, GlTab, GlBadge } from '@gitlab/ui'; + +export default { + components: { + GlTabs, + GlTab, + GlBadge, + }, + props: { + tabs: { + type: Array, + required: true, + }, + tabCounts: { + type: Object, + required: true, + }, + currentTab: { + type: String, + required: true, + }, + }, + methods: { + isTabActive(tabName) { + return tabName === this.currentTab; + }, + }, +}; +</script> + +<template> + <div class="top-area"> + <gl-tabs class="nav-links mobile-separator issuable-state-filters"> + <gl-tab + v-for="tab in tabs" + :key="tab.id" + :active="isTabActive(tab.name)" + @click="$emit('click', tab.name)" + > + <template #title> + <span :title="tab.titleTooltip">{{ tab.title }}</span> + <gl-badge variant="neutral" size="sm" class="gl-px-2 gl-py-1!">{{ + tabCounts[tab.name] + }}</gl-badge> + </template> + </gl-tab> + </gl-tabs> + <div class="nav-controls"> + <slot name="nav-actions"></slot> + </div> + </div> +</template> diff --git a/app/assets/javascripts/issuable_suggestions/components/app.vue b/app/assets/javascripts/issuable_suggestions/components/app.vue index 810ca7ac1bd..8a9a880e7ee 100644 --- a/app/assets/javascripts/issuable_suggestions/components/app.vue +++ b/app/assets/javascripts/issuable_suggestions/components/app.vue @@ -1,14 +1,13 @@ <script> -import { GlTooltipDirective } from '@gitlab/ui'; +import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; import Suggestion from './item.vue'; import query from '../queries/issues.query.graphql'; export default { components: { Suggestion, - Icon, + GlIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -70,7 +69,7 @@ export default { <div v-show="showSuggestions" class="form-group row issuable-suggestions"> <div v-once class="col-form-label col-sm-2 pt-0"> {{ __('Similar issues') }} - <icon + <gl-icon v-gl-tooltip.bottom :title="$options.helpText" :aria-label="$options.helpText" diff --git a/app/assets/javascripts/issuable_suggestions/components/item.vue b/app/assets/javascripts/issuable_suggestions/components/item.vue index dfadb9d2b24..6e265b1bf42 100644 --- a/app/assets/javascripts/issuable_suggestions/components/item.vue +++ b/app/assets/javascripts/issuable_suggestions/components/item.vue @@ -1,9 +1,8 @@ <script> /* eslint-disable @gitlab/vue-require-i18n-strings */ import { uniqueId } from 'lodash'; -import { GlLink, GlTooltip, GlTooltipDirective } from '@gitlab/ui'; +import { GlLink, GlTooltip, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import timeago from '~/vue_shared/mixins/timeago'; @@ -12,7 +11,7 @@ export default { components: { GlTooltip, GlLink, - Icon, + GlIcon, UserAvatarImage, TimeagoTooltip, }, @@ -68,7 +67,7 @@ export default { <template> <div class="suggestion-item"> <div class="d-flex align-items-center"> - <icon + <gl-icon v-if="suggestion.confidential" v-gl-tooltip.bottom :title="__('Confidential')" @@ -84,7 +83,7 @@ export default { </gl-link> </div> <div class="text-secondary suggestion-footer"> - <icon + <gl-icon ref="state" :name="stateIcon" :class="{ @@ -134,7 +133,7 @@ export default { :title="tooltipTitle" class="suggestion-help-hover gl-ml-3 text-tertiary" > - <icon :name="icon" /> {{ count }} + <gl-icon :name="icon" /> {{ count }} </span> </span> </div> diff --git a/app/assets/javascripts/issuables_list/service_desk_helper.js b/app/assets/javascripts/issuables_list/service_desk_helper.js deleted file mode 100644 index 4b4a38c2205..00000000000 --- a/app/assets/javascripts/issuables_list/service_desk_helper.js +++ /dev/null @@ -1,55 +0,0 @@ -import { __ } from '~/locale'; - -/** - * Returns the attributes used for gl-empty-state in the Service Desk issues list. - */ -// eslint-disable-next-line import/prefer-default-export -export function emptyStateHelper(emptyStateMeta) { - const { isServiceDeskSupported, svgPath, serviceDeskHelpPage } = emptyStateMeta; - - if (isServiceDeskSupported) { - const title = __( - 'Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab', - ); - const commonMessage = __( - 'Those emails automatically become issues (with the comments becoming the email conversation) listed here.', - ); - const commonDescription = ` - <span>${commonMessage}</span> - <a href="${serviceDeskHelpPage}">${__('Read more')}</a>`; - - if (emptyStateMeta.canEditProjectSettings && emptyStateMeta.isServiceDeskEnabled) { - return { - title, - svgPath, - description: `<p>${__('Have your users email')} <code>${ - emptyStateMeta.serviceDeskAddress - }</code></p> ${commonDescription}`, - }; - } - - if (emptyStateMeta.canEditProjectSettings && !emptyStateMeta.isServiceDeskEnabled) { - return { - title, - svgPath, - description: commonDescription, - primaryLink: emptyStateMeta.editProjectPage, - primaryText: __('Turn on Service Desk'), - }; - } - - return { - title, - svgPath, - description: commonDescription, - }; - } - - return { - title: __('Service Desk is enabled but not yet active'), - svgPath, - description: __('You must set up incoming email before it becomes active.'), - primaryLink: emptyStateMeta.incomingEmailHelpPage, - primaryText: __('More information'), - }; -} diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index f1b37525a6d..0a0cfe918af 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -100,6 +100,13 @@ export default class Issue { initIssueBtnEventListeners() { const issueFailMessage = __('Unable to update this issue at this time.'); + $('.report-abuse-link').on('click', e => { + // this is needed because of the implementation of + // the dropdown toggle and Report Abuse needing to be + // linked to another page. + e.stopPropagation(); + }); + // NOTE: data attribute seems unnecessary but is actually necessary return $('.js-issuable-buttons[data-action="close-reopen"]').on( 'click', @@ -173,11 +180,15 @@ export default class Issue { } initIssueWarningBtnEventListener() { - return $(document).on('click', '.js-close-blocked-issue-warning button.btn-secondary', e => { - e.preventDefault(); - e.stopImmediatePropagation(); - this.toggleWarningAndCloseButton(); - }); + return $(document).on( + 'click', + '.js-close-blocked-issue-warning .js-cancel-blocked-issue-warning', + e => { + e.preventDefault(); + e.stopImmediatePropagation(); + this.toggleWarningAndCloseButton(); + }, + ); } initIssueMovedFromServiceDeskDismissHandler() { diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index 992d87a969f..22db0f1cfc1 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -20,7 +20,6 @@ export default { components: { GlIcon, GlIntersectionObserver, - descriptionComponent, titleComponent, editedComponent, formComponent, @@ -152,6 +151,18 @@ export default { required: false, default: 0, }, + descriptionComponent: { + type: Object, + required: false, + default: () => { + return descriptionComponent; + }, + }, + showTitleBorder: { + type: Boolean, + required: false, + default: true, + }, }, data() { const store = new Store({ @@ -209,6 +220,11 @@ export default { isOpenStatus() { return this.issuableStatus === IssuableStatus.Open; }, + pinnedLinkClasses() { + return this.showTitleBorder + ? 'gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-mb-6' + : ''; + }, statusIcon() { return this.isOpenStatus ? 'issue-open-m' : 'mobile-issue-close'; }, @@ -231,7 +247,7 @@ export default { }); if (!Visibility.hidden()) { - this.poll.makeRequest(); + this.poll.makeDelayedRequest(2000); } Visibility.change(() => { @@ -447,10 +463,11 @@ export default { <pinned-links :zoom-meeting-url="zoomMeetingUrl" :published-incident-url="publishedIncidentUrl" + :class="pinnedLinkClasses" /> - <description-component - v-if="state.descriptionHtml" + <component + :is="descriptionComponent" :can-update="canUpdate" :description-html="state.descriptionHtml" :description-text="state.descriptionText" diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index abb63f606ae..2a6468c783b 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -1,5 +1,6 @@ <script> import $ from 'jquery'; +import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import animateMixin from '../mixins/animate'; @@ -7,6 +8,10 @@ import TaskList from '../../task_list'; import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; export default { + directives: { + SafeHtml, + }, + mixins: [animateMixin, recaptchaModalImplementor], props: { @@ -20,7 +25,8 @@ export default { }, descriptionText: { type: String, - required: true, + required: false, + default: '', }, taskStatus: { type: String, @@ -47,11 +53,16 @@ export default { return { preAnimation: false, pulseAnimation: false, + initialUpdate: true, }; }, watch: { - descriptionHtml() { - this.animateChange(); + descriptionHtml(newDescription, oldDescription) { + if (!this.initialUpdate && newDescription !== oldDescription) { + this.animateChange(); + } else { + this.initialUpdate = false; + } this.$nextTick(() => { this.renderGFM(); @@ -136,12 +147,12 @@ export default { > <div ref="gfm-content" + v-safe-html="descriptionHtml" :class="{ 'issue-realtime-pre-pulse': preAnimation, 'issue-realtime-trigger-pulse': pulseAnimation, }" class="md" - v-html="descriptionHtml" ></div> <textarea v-if="descriptionText" diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue index 4ee44e50d2f..14ada5adcf6 100644 --- a/app/assets/javascripts/issue_show/components/edit_actions.vue +++ b/app/assets/javascripts/issue_show/components/edit_actions.vue @@ -1,5 +1,5 @@ <script> -/* eslint-disable @gitlab/vue-require-i18n-strings */ +import { GlButton } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import updateMixin from '../mixins/update'; import eventHub from '../event_hub'; @@ -10,6 +10,9 @@ const issuableTypes = { }; export default { + components: { + GlButton, + }, mixins: [updateMixin], props: { canDestroy: { @@ -64,28 +67,30 @@ export default { <template> <div class="gl-mt-3 gl-mb-3 clearfix"> - <button - :class="{ disabled: formState.updateLoading || !isSubmitEnabled }" + <gl-button + :loading="formState.updateLoading" :disabled="formState.updateLoading || !isSubmitEnabled" - class="btn btn-success float-left qa-save-button" + category="primary" + variant="success" + class="float-left qa-save-button" type="submit" @click.prevent="updateIssuable" > - Save changes - <i v-if="formState.updateLoading" class="fa fa-spinner fa-spin" aria-hidden="true"> </i> - </button> - <button class="btn btn-default float-right" type="button" @click="closeForm"> + {{ __('Save changes') }} + </gl-button> + <gl-button class="float-right" @click="closeForm"> {{ __('Cancel') }} - </button> - <button + </gl-button> + <gl-button v-if="shouldShowDeleteButton" - :class="{ disabled: deleteLoading }" + :loading="deleteLoading" :disabled="deleteLoading" - class="btn btn-danger float-right gl-mr-3 qa-delete-button" - type="button" + category="primary" + variant="danger" + class="float-right gl-mr-3 qa-delete-button" @click="deleteIssuable" > - Delete <i v-if="deleteLoading" class="fa fa-spinner fa-spin" aria-hidden="true"> </i> - </button> + {{ __('Delete') }} + </gl-button> </div> </template> 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 6d8a9950b6d..e1b308c6f57 100644 --- a/app/assets/javascripts/issue_show/components/fields/description_template.vue +++ b/app/assets/javascripts/issue_show/components/fields/description_template.vue @@ -1,9 +1,13 @@ <script> /* eslint-disable @gitlab/vue-require-i18n-strings */ import $ from 'jquery'; +import { GlIcon } from '@gitlab/ui'; import IssuableTemplateSelectors from '../../../templates/issuable_template_selectors'; export default { + components: { + GlIcon, + }, props: { formState: { type: Object, @@ -61,14 +65,14 @@ export default { <i aria-hidden="true" class="fa fa-chevron-down"> </i> </button> <div class="dropdown-menu dropdown-select"> - <div class="dropdown-title"> - Choose a template + <div class="dropdown-title gl-display-flex gl-justify-content-center"> + <span class="gl-ml-auto">Choose a template</span> <button - class="dropdown-title-button dropdown-menu-close" + class="dropdown-title-button dropdown-menu-close gl-ml-auto" :aria-label="__('Close')" type="button" > - <i aria-hidden="true" class="fa fa-times dropdown-menu-close-icon"> </i> + <gl-icon name="close" class="dropdown-menu-close-icon" :aria-hidden="true" /> </button> </div> <div class="dropdown-input"> @@ -79,12 +83,11 @@ export default { autocomplete="off" /> <i aria-hidden="true" class="fa fa-search dropdown-input-search"> </i> - <i - role="button" + <gl-icon + name="close" + class="dropdown-input-clear js-dropdown-input-clear" :aria-label="__('Clear templates search input')" - class="fa fa-times dropdown-input-clear js-dropdown-input-clear" - > - </i> + /> </div> <div class="dropdown-content"></div> <div class="dropdown-footer"> diff --git a/app/assets/javascripts/issue_show/components/incidents/graphql/queries/get_alert.graphql b/app/assets/javascripts/issue_show/components/incidents/graphql/queries/get_alert.graphql new file mode 100644 index 00000000000..00ddc80432d --- /dev/null +++ b/app/assets/javascripts/issue_show/components/incidents/graphql/queries/get_alert.graphql @@ -0,0 +1,20 @@ +query getAlert($iid: String!, $fullPath: ID!) { + project(fullPath: $fullPath) { + issue(iid: $iid) { + alertManagementAlert { + iid + title + detailsUrl + severity + status + startedAt + eventCount + monitoringTool + service + description + endedAt + details + } + } + } +} diff --git a/app/assets/javascripts/issue_show/components/incidents/highlight_bar.vue b/app/assets/javascripts/issue_show/components/incidents/highlight_bar.vue new file mode 100644 index 00000000000..a47fe4c84cf --- /dev/null +++ b/app/assets/javascripts/issue_show/components/incidents/highlight_bar.vue @@ -0,0 +1,42 @@ +<script> +import { GlLink } from '@gitlab/ui'; +import { formatDate } from '~/lib/utils/datetime_utility'; + +export default { + components: { + GlLink, + }, + props: { + alert: { + type: Object, + required: true, + }, + }, + computed: { + startTime() { + return formatDate(this.alert.startedAt, 'yyyy-mm-dd Z'); + }, + }, +}; +</script> + +<template> + <div + class="gl-border-solid gl-border-1 gl-border-gray-100 gl-p-5 gl-mb-3 gl-rounded-base gl-display-flex gl-justify-content-space-between" + > + <div class="text-truncate gl-pr-3"> + <span class="gl-font-weight-bold">{{ s__('HighlightBar|Original alert:') }}</span> + <gl-link :href="alert.detailsUrl">{{ alert.title }}</gl-link> + </div> + + <div class="gl-pr-3 gl-white-space-nowrap"> + <span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert start time:') }}</span> + {{ startTime }} + </div> + + <div class="gl-white-space-nowrap"> + <span class="gl-font-weight-bold">{{ s__('HighlightBar|Alert events:') }}</span> + <span>{{ alert.eventCount }}</span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue b/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue new file mode 100644 index 00000000000..4104ddbf06f --- /dev/null +++ b/app/assets/javascripts/issue_show/components/incidents/incident_tabs.vue @@ -0,0 +1,71 @@ +<script> +import { GlTab, GlTabs } from '@gitlab/ui'; +import DescriptionComponent from '../description.vue'; +import HighlightBar from './highlight_bar.vue'; +import createFlash from '~/flash'; +import { s__ } from '~/locale'; +import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; + +import getAlert from './graphql/queries/get_alert.graphql'; + +export default { + components: { + AlertDetailsTable, + DescriptionComponent, + GlTab, + GlTabs, + HighlightBar, + }, + inject: ['fullPath', 'iid'], + apollo: { + alert: { + query: getAlert, + variables() { + return { + fullPath: this.fullPath, + iid: this.iid, + }; + }, + update(data) { + return data?.project?.issue?.alertManagementAlert; + }, + error() { + createFlash({ + message: s__('Incident|There was an issue loading alert data. Please try again.'), + }); + }, + }, + }, + data() { + return { + alert: null, + }; + }, + computed: { + loading() { + return this.$apollo.queries.alert.loading; + }, + alertTableFields() { + if (this.alert) { + const { detailsUrl, __typename, ...restDetails } = this.alert; + return restDetails; + } + return null; + }, + }, +}; +</script> + +<template> + <div> + <gl-tabs content-class="gl-reset-line-height" class="gl-mt-n3" data-testid="incident-tabs"> + <gl-tab :title="s__('Incident|Summary')"> + <highlight-bar v-if="alert" :alert="alert" /> + <description-component v-bind="$attrs" /> + </gl-tab> + <gl-tab v-if="alert" class="alert-management-details" :title="s__('Incident|Alert details')"> + <alert-details-table :alert="alertTableFields" :loading="loading" /> + </gl-tab> + </gl-tabs> + </div> +</template> diff --git a/app/assets/javascripts/issue_show/components/locked_warning.vue b/app/assets/javascripts/issue_show/components/locked_warning.vue index 19c7a11d87b..96f5a7c88e0 100644 --- a/app/assets/javascripts/issue_show/components/locked_warning.vue +++ b/app/assets/javascripts/issue_show/components/locked_warning.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { __, sprintf } from '~/locale'; export default { diff --git a/app/assets/javascripts/issue_show/components/pinned_links.vue b/app/assets/javascripts/issue_show/components/pinned_links.vue index a877aa2ac96..d38189307bd 100644 --- a/app/assets/javascripts/issue_show/components/pinned_links.vue +++ b/app/assets/javascripts/issue_show/components/pinned_links.vue @@ -20,20 +20,25 @@ export default { }, computed: { pinnedLinks() { - return [ - { + const links = []; + if (this.publishedIncidentUrl) { + links.push({ id: 'publishedIncidentUrl', url: this.publishedIncidentUrl, text: STATUS_PAGE_PUBLISHED, icon: 'tanuki', - }, - { + }); + } + if (this.zoomMeetingUrl) { + links.push({ id: 'zoomMeetingUrl', url: this.zoomMeetingUrl, text: JOIN_ZOOM_MEETING, icon: 'brand-zoom', - }, - ]; + }); + } + + return links; }, }, methods: { @@ -45,7 +50,7 @@ export default { </script> <template> - <div class="border-bottom gl-mb-6 gl-display-flex gl-justify-content-start"> + <div v-if="pinnedLinks && pinnedLinks.length" class="gl-display-flex gl-justify-content-start"> <template v-for="(link, i) in pinnedLinks"> <div v-if="link.url" :key="link.id" :class="{ 'gl-pr-3': needsPaddingClass(i) }"> <gl-button diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue index 1e1dce5f4fc..b03a91716fe 100644 --- a/app/assets/javascripts/issue_show/components/title.vue +++ b/app/assets/javascripts/issue_show/components/title.vue @@ -1,12 +1,15 @@ <script> +import { GlButton, GlTooltipDirective, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import animateMixin from '../mixins/animate'; import eventHub from '../event_hub'; -import tooltip from '../../vue_shared/directives/tooltip'; -import { spriteIcon } from '../../lib/utils/common_utils'; export default { + components: { + GlButton, + }, directives: { - tooltip, + GlTooltip: GlTooltipDirective, + SafeHtml, }, mixins: [animateMixin], props: { @@ -40,11 +43,6 @@ export default { titleEl: document.querySelector('title'), }; }, - computed: { - pencilIcon() { - return spriteIcon('pencil', 'link-highlight'); - }, - }, watch: { titleHtml() { this.setPageTitle(); @@ -67,25 +65,21 @@ export default { <template> <div class="title-container"> <h2 + v-safe-html="titleHtml" :class="{ 'issue-realtime-pre-pulse': preAnimation, 'issue-realtime-trigger-pulse': pulseAnimation, }" class="title qa-title" dir="auto" - v-html="titleHtml" ></h2> - <button + <gl-button v-if="showInlineEditButton && canUpdate" - v-tooltip - type="button" - class="btn btn-default btn-edit btn-svg js-issuable-edit - qa-edit-button" + v-gl-tooltip.bottom + icon="pencil" + class="btn-edit js-issuable-edit qa-edit-button" title="Edit title and description" - data-placement="bottom" - data-container="body" @click="edit" - v-html="pencilIcon" - ></button> + /> </div> </template> diff --git a/app/assets/javascripts/issue_show/incident.js b/app/assets/javascripts/issue_show/incident.js new file mode 100644 index 00000000000..a34e75ee64a --- /dev/null +++ b/app/assets/javascripts/issue_show/incident.js @@ -0,0 +1,36 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import issuableApp from './components/app.vue'; +import incidentTabs from './components/incidents/incident_tabs.vue'; + +Vue.use(VueApollo); + +export default function initIssuableApp(issuableData = {}) { + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + const { projectNamespace, projectPath, iid } = issuableData; + + return new Vue({ + el: document.getElementById('js-issuable-app'), + apolloProvider, + components: { + issuableApp, + }, + provide: { + fullPath: `${projectNamespace}/${projectPath}`, + iid, + }, + render(createElement) { + return createElement('issuable-app', { + props: { + ...issuableData, + descriptionComponent: incidentTabs, + showTitleBorder: false, + }, + }); + }, + }); +} diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/issue.js index e170d338408..f9f61d5aa64 100644 --- a/app/assets/javascripts/issue_show/index.js +++ b/app/assets/javascripts/issue_show/issue.js @@ -1,8 +1,7 @@ import Vue from 'vue'; import issuableApp from './components/app.vue'; -import { parseIssuableData } from './utils/parse_data'; -export default function initIssueableApp() { +export default function initIssuableApp(issuableData) { return new Vue({ el: document.getElementById('js-issuable-app'), components: { @@ -10,7 +9,7 @@ export default function initIssueableApp() { }, render(createElement) { return createElement('issuable-app', { - props: parseIssuableData(), + props: issuableData, }); }, }); diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js index 0cd094243b9..c6f7e892f9b 100644 --- a/app/assets/javascripts/issue_show/stores/index.js +++ b/app/assets/javascripts/issue_show/stores/index.js @@ -1,3 +1,4 @@ +import { sanitize } from 'dompurify'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import updateDescription from '../utils/update_description'; @@ -27,8 +28,8 @@ export default class Store { const details = descriptionSection != null && descriptionSection.getElementsByTagName('details'); - this.state.descriptionHtml = updateDescription(data.description, details); - this.state.titleHtml = data.title; + this.state.descriptionHtml = updateDescription(sanitize(data.description), details); + this.state.titleHtml = sanitize(data.title); this.state.lock_version = data.lock_version; } diff --git a/app/assets/javascripts/issue_show/utils/parse_data.js b/app/assets/javascripts/issue_show/utils/parse_data.js index 8cd1c1b0e56..a62a5167961 100644 --- a/app/assets/javascripts/issue_show/utils/parse_data.js +++ b/app/assets/javascripts/issue_show/utils/parse_data.js @@ -1,10 +1,22 @@ import { sanitize } from 'dompurify'; +// We currently load + parse the data from the issue app and related merge request +let cachedParsedData; + export const parseIssuableData = () => { try { + if (cachedParsedData) return cachedParsedData; + const initialDataEl = document.getElementById('js-issuable-app-initial-data'); - return JSON.parse(sanitize(initialDataEl.textContent).replace(/"/g, '"')); + const parsedData = JSON.parse(initialDataEl.textContent.replace(/"/g, '"')); + + parsedData.initialTitleHtml = sanitize(parsedData.initialTitleHtml); + parsedData.initialDescriptionHtml = sanitize(parsedData.initialDescriptionHtml); + + cachedParsedData = parsedData; + + return parsedData; } catch (e) { console.error(e); // eslint-disable-line no-console diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js index 75edff41a89..02b10730153 100644 --- a/app/assets/javascripts/issue_status_select.js +++ b/app/assets/javascripts/issue_status_select.js @@ -1,10 +1,11 @@ import $ from 'jquery'; import { __ } from './locale'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default function issueStatusSelect() { $('.js-issue-status').each((i, el) => { const fieldName = $(el).data('fieldName'); - return $(el).glDropdown({ + initDeprecatedJQueryDropdown($(el), { selectable: true, fieldName, toggleLabel(selected, element, instance) { diff --git a/app/assets/javascripts/issuables_list/components/issuable.vue b/app/assets/javascripts/issues_list/components/issuable.vue index 4fc614f8da4..adfb234fe7a 100644 --- a/app/assets/javascripts/issuables_list/components/issuable.vue +++ b/app/assets/javascripts/issues_list/components/issuable.vue @@ -6,7 +6,14 @@ // TODO: need to move this component to graphql - https://gitlab.com/gitlab-org/gitlab/-/issues/221246 import { escape, isNumber } from 'lodash'; -import { GlLink, GlTooltipDirective as GlTooltip, GlSprintf, GlLabel, GlIcon } from '@gitlab/ui'; +import { + GlLink, + GlTooltipDirective as GlTooltip, + GlSprintf, + GlLabel, + GlIcon, + GlSafeHtmlDirective as SafeHtml, +} from '@gitlab/ui'; import jiraLogo from '@gitlab/svgs/dist/illustrations/logos/jira.svg'; import { dateInWords, @@ -41,6 +48,7 @@ export default { }, directives: { GlTooltip, + SafeHtml, }, mixins: [glFeatureFlagsMixin()], props: { @@ -157,7 +165,7 @@ export default { value: this.issuable.merge_requests_count, title: __('Related merge requests'), dataTestId: 'merge-requests', - class: 'js-merge-requests icon-merge-request-unmerged', + class: 'js-merge-requests', icon: 'merge-request', }, { @@ -298,9 +306,9 @@ export default { <span class="js-ref-path gl-mr-4 mr-sm-0"> <span v-if="isJiraIssue" + v-safe-html="jiraLogo" class="svg-container jira-logo-container" data-testid="jira-logo" - v-html="jiraLogo" ></span> {{ referencePath }} </span> diff --git a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue b/app/assets/javascripts/issues_list/components/issuables_list_app.vue index fecb7353efb..0d4f5bce965 100644 --- a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue +++ b/app/assets/javascripts/issues_list/components/issuables_list_app.vue @@ -3,7 +3,7 @@ import { toNumber, omit } from 'lodash'; import { GlEmptyState, GlPagination, - GlSkeletonLoading, + GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSafeHtmlDirective as SafeHtml, } from '@gitlab/ui'; import { deprecatedCreateFlash as flash } from '~/flash'; diff --git a/app/assets/javascripts/issuables_list/components/issuable_list_root_app.vue b/app/assets/javascripts/issues_list/components/jira_issues_list_root.vue index cc90d23eda7..61781c576c0 100644 --- a/app/assets/javascripts/issuables_list/components/issuable_list_root_app.vue +++ b/app/assets/javascripts/issues_list/components/jira_issues_list_root.vue @@ -11,7 +11,7 @@ import { } from '~/jira_import/utils/jira_import_utils'; export default { - name: 'IssuableListRoot', + name: 'JiraIssuesList', components: { GlAlert, GlLabel, diff --git a/app/assets/javascripts/issuables_list/constants.js b/app/assets/javascripts/issues_list/constants.js index f008ba1bf4a..f008ba1bf4a 100644 --- a/app/assets/javascripts/issuables_list/constants.js +++ b/app/assets/javascripts/issues_list/constants.js diff --git a/app/assets/javascripts/issuables_list/eventhub.js b/app/assets/javascripts/issues_list/eventhub.js index e31806ad199..e31806ad199 100644 --- a/app/assets/javascripts/issuables_list/eventhub.js +++ b/app/assets/javascripts/issues_list/eventhub.js diff --git a/app/assets/javascripts/issuables_list/index.js b/app/assets/javascripts/issues_list/index.js index fa23d6c0eed..1ff41c20d08 100644 --- a/app/assets/javascripts/issuables_list/index.js +++ b/app/assets/javascripts/issues_list/index.js @@ -2,10 +2,10 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import IssuableListRootApp from './components/issuable_list_root_app.vue'; +import JiraIssuesListRoot from './components/jira_issues_list_root.vue'; import IssuablesListApp from './components/issuables_list_app.vue'; -function mountIssuableListRootApp() { +function mountJiraIssuesListApp() { const el = document.querySelector('.js-projects-issues-root'); if (!el) { @@ -23,7 +23,7 @@ function mountIssuableListRootApp() { el, apolloProvider, render(createComponent) { - return createComponent(IssuableListRootApp, { + return createComponent(JiraIssuesListRoot, { props: { canEdit: parseBoolean(el.dataset.canEdit), isJiraConfigured: parseBoolean(el.dataset.isJiraConfigured), @@ -62,6 +62,6 @@ function mountIssuablesListApp() { } export default function initIssuablesList() { - mountIssuableListRootApp(); + mountJiraIssuesListApp(); mountIssuablesListApp(); } diff --git a/app/assets/javascripts/issuables_list/queries/get_issues_list_details.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql index 8f9b888d19b..8f9b888d19b 100644 --- a/app/assets/javascripts/issuables_list/queries/get_issues_list_details.query.graphql +++ b/app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql diff --git a/app/assets/javascripts/issues_list/service_desk_helper.js b/app/assets/javascripts/issues_list/service_desk_helper.js new file mode 100644 index 00000000000..0a34b754377 --- /dev/null +++ b/app/assets/javascripts/issues_list/service_desk_helper.js @@ -0,0 +1,111 @@ +import { __ } from '~/locale'; + +/** + * Generates empty state messages for Service Desk issues list. + * + * @param {emptyStateMeta} emptyStateMeta - Meta data used to generate empty state messages + * @returns {Object} Object containing empty state messages generated using the meta data. + */ +export function generateMessages(emptyStateMeta) { + const { + svgPath, + serviceDeskHelpPage, + serviceDeskAddress, + editProjectPage, + incomingEmailHelpPage, + } = emptyStateMeta; + + const serviceDeskSupportedTitle = __( + 'Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab', + ); + + const serviceDeskSupportedMessage = __( + 'Those emails automatically become issues (with the comments becoming the email conversation) listed here.', + ); + + const commonDescription = ` + <span>${serviceDeskSupportedMessage}</span> + <a href="${serviceDeskHelpPage}">${__('Read more')}</a>`; + + return { + serviceDeskEnabledAndCanEditProjectSettings: { + title: serviceDeskSupportedTitle, + svgPath, + description: `<p>${__('Have your users email')} + <code>${serviceDeskAddress}</code> + </p> + ${commonDescription}`, + }, + serviceDeskEnabledAndCannotEditProjectSettings: { + title: serviceDeskSupportedTitle, + svgPath, + description: commonDescription, + }, + serviceDeskDisabledAndCanEditProjectSettings: { + title: serviceDeskSupportedTitle, + svgPath, + description: commonDescription, + primaryLink: editProjectPage, + primaryText: __('Turn on Service Desk'), + }, + serviceDeskDisabledAndCannotEditProjectSettings: { + title: serviceDeskSupportedTitle, + svgPath, + description: commonDescription, + }, + serviceDeskIsNotSupported: { + title: __('Service Desk is not supported'), + svgPath, + description: __( + 'In order to enable Service Desk for your instance, you must first set up incoming email.', + ), + primaryLink: incomingEmailHelpPage, + primaryText: __('More information'), + }, + serviceDeskIsNotEnabled: { + title: __('Service Desk is not enabled'), + svgPath, + description: __( + 'For help setting up the Service Desk for your instance, please contact an administrator.', + ), + }, + }; +} + +/** + * Returns the attributes used for gl-empty-state in the Service Desk issues list. + * + * @param {Object} emptyStateMeta - Meta data used to generate empty state messages + * @returns {Object} + */ +export function emptyStateHelper(emptyStateMeta) { + const messages = generateMessages(emptyStateMeta); + + const { isServiceDeskSupported, canEditProjectSettings, isServiceDeskEnabled } = emptyStateMeta; + + if (isServiceDeskSupported) { + if (isServiceDeskEnabled && canEditProjectSettings) { + return messages.serviceDeskEnabledAndCanEditProjectSettings; + } + + if (isServiceDeskEnabled && !canEditProjectSettings) { + return messages.serviceDeskEnabledAndCannotEditProjectSettings; + } + + // !isServiceDeskEnabled && canEditProjectSettings + if (canEditProjectSettings) { + return messages.serviceDeskDisabledAndCanEditProjectSettings; + } + + // !isServiceDeskEnabled && !canEditProjectSettings + return messages.serviceDeskDisabledAndCannotEditProjectSettings; + } + + // !serviceDeskSupported && canEditProjectSettings + if (canEditProjectSettings) { + return messages.serviceDeskIsNotSupported; + } + + // !serviceDeskSupported && !canEditProjectSettings + return messages.serviceDeskIsNotEnabled; +} diff --git a/app/assets/javascripts/jira_connect.js b/app/assets/javascripts/jira_connect.js new file mode 100644 index 00000000000..895cdc4562c --- /dev/null +++ b/app/assets/javascripts/jira_connect.js @@ -0,0 +1,56 @@ +/* eslint-disable func-names, no-var, no-alert */ +/* global $ */ +/* global AP */ + +/** + * This script is not going through Webpack bundling + * as it is only included in `app/views/jira_connect/subscriptions/index.html.haml` + * which is going to be rendered within iframe on Jira app dashboard + * hence any code written here needs to be IE11+ compatible (no fully ES6) + */ + +function onLoaded() { + var reqComplete = function() { + AP.navigator.reload(); + }; + + var reqFailed = function(res) { + alert(res.responseJSON.error); + }; + + $('#add-subscription-form').on('submit', function(e) { + var actionUrl = $(this).attr('action'); + e.preventDefault(); + + AP.context.getToken(function(token) { + // eslint-disable-next-line no-jquery/no-ajax + $.post(actionUrl, { + jwt: token, + namespace_path: $('#namespace-input').val(), + format: 'json', + }) + .done(reqComplete) + .fail(reqFailed); + }); + }); + + $('.remove-subscription').on('click', function(e) { + var href = $(this).attr('href'); + e.preventDefault(); + + AP.context.getToken(function(token) { + // eslint-disable-next-line no-jquery/no-ajax + $.ajax({ + url: href, + method: 'DELETE', + data: { + jwt: token, + format: 'json', + }, + }) + .done(reqComplete) + .fail(reqFailed); + }); + }); +} +document.addEventListener('DOMContentLoaded', onLoaded); 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 5bf98682f92..4339021d9a0 100644 --- a/app/assets/javascripts/jira_import/components/jira_import_form.vue +++ b/app/assets/javascripts/jira_import/components/jira_import_form.vue @@ -2,9 +2,9 @@ import { GlAlert, GlButton, - GlNewDropdown, - GlNewDropdownItem, - GlNewDropdownText, + GlDropdown, + GlDropdownItem, + GlDropdownText, GlFormGroup, GlFormSelect, GlIcon, @@ -34,9 +34,9 @@ export default { components: { GlAlert, GlButton, - GlNewDropdown, - GlNewDropdownItem, - GlNewDropdownText, + GlDropdown, + GlDropdownItem, + GlDropdownText, GlFormGroup, GlFormSelect, GlIcon, @@ -293,7 +293,7 @@ export default { <gl-icon name="arrow-right" :aria-label="__('Will be mapped to')" /> </template> <template #cell(gitlabUsername)="data"> - <gl-new-dropdown + <gl-dropdown :text="data.value || $options.currentUsername" class="w-100" :aria-label=" @@ -301,23 +301,23 @@ export default { " @hide="resetDropdown" > - <gl-search-box-by-type v-model.trim="searchTerm" class="m-2" /> + <gl-search-box-by-type v-model.trim="searchTerm" class="gl-m-3" /> <gl-loading-icon v-if="isFetching" /> - <gl-new-dropdown-item + <gl-dropdown-item v-for="user in users" v-else :key="user.id" @click="updateMapping(data.item.jiraAccountId, user.id, user.username)" > {{ user.username }} ({{ user.name }}) - </gl-new-dropdown-item> + </gl-dropdown-item> - <gl-new-dropdown-text v-show="shouldShowNoMatchesFoundText" class="text-secondary"> + <gl-dropdown-text v-show="shouldShowNoMatchesFoundText" class="text-secondary"> {{ __('No matches found') }} - </gl-new-dropdown-text> - </gl-new-dropdown> + </gl-dropdown-text> + </gl-dropdown> </template> </gl-table> 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 a1186b087e1..edd6fad4aac 100644 --- a/app/assets/javascripts/jira_import/utils/jira_import_utils.js +++ b/app/assets/javascripts/jira_import/utils/jira_import_utils.js @@ -1,5 +1,5 @@ import { last } from 'lodash'; -import { JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY } from '~/issuables_list/constants'; +import { JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY } from '~/issues_list/constants'; export const IMPORT_STATE = { FAILED: 'failed', diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue index 6183779acd4..2850a8e86fd 100644 --- a/app/assets/javascripts/jobs/components/artifacts_block.vue +++ b/app/assets/javascripts/jobs/components/artifacts_block.vue @@ -1,11 +1,12 @@ <script> -import { GlLink } from '@gitlab/ui'; +import { GlIcon, GlLink } from '@gitlab/ui'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; export default { components: { TimeagoTooltip, + GlIcon, GlLink, }, mixins: [timeagoMixin], @@ -14,6 +15,10 @@ export default { type: Object, required: true, }, + helpUrl: { + type: String, + required: true, + }, }, computed: { isExpired() { @@ -40,6 +45,14 @@ export default { <span v-if="isExpired">{{ s__('Job|The artifacts were removed') }}</span> <span v-if="willExpire">{{ s__('Job|The artifacts will be removed') }}</span> <timeago-tooltip v-if="artifact.expire_at" :time="artifact.expire_at" /> + <gl-link + :href="helpUrl" + target="_blank" + rel="noopener noreferrer nofollow" + data-testid="artifact-expired-help-link" + > + <gl-icon name="question" /> + </gl-link> </p> <p v-else-if="isLocked" class="build-detail-row"> <span data-testid="job-locked-message">{{ @@ -71,6 +84,7 @@ export default { :href="artifact.browse_path" class="btn btn-sm btn-default" data-testid="browse-artifacts" + data-qa-selector="browse_artifacts_button" >{{ s__('Job|Browse') }}</gl-link > </div> diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index e760706c97e..00ff3fb939d 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -1,13 +1,13 @@ <script> +/* eslint-disable vue/no-v-html */ import { throttle, isEmpty } from 'lodash'; import { mapGetters, mapState, mapActions } from 'vuex'; -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; import { polyfillSticky } from '~/lib/utils/sticky'; import CiHeader from '~/vue_shared/components/header_ci_component.vue'; import Callout from '~/vue_shared/components/callout.vue'; -import Icon from '~/vue_shared/components/icon.vue'; import EmptyState from './empty_state.vue'; import EnvironmentsBlock from './environments_block.vue'; import ErasedBlock from './erased_block.vue'; @@ -27,7 +27,7 @@ export default { EmptyState, EnvironmentsBlock, ErasedBlock, - Icon, + GlIcon, Log, LogTopBar, StuckBlock, @@ -38,6 +38,11 @@ export default { }, mixins: [delayedJobMixin], props: { + artifactHelpUrl: { + type: String, + required: false, + default: '', + }, runnerSettingsUrl: { type: String, required: false, @@ -266,7 +271,7 @@ export default { :class="{ 'sticky-top border-bottom-0': hasTrace }" data-testid="archived-job" > - <icon name="lock" class="align-text-bottom" /> + <gl-icon name="lock" class="align-text-bottom" /> {{ __('This job is archived. Only the complete pipeline can be retried.') }} </div> <!-- job log --> @@ -319,6 +324,7 @@ export default { 'right-sidebar-expanded': isSidebarOpen, 'right-sidebar-collapsed': !isSidebarOpen, }" + :artifact-help-url="artifactHelpUrl" :runner-help-url="runnerHelpUrl" data-testid="job-sidebar" /> diff --git a/app/assets/javascripts/jobs/components/job_container_item.vue b/app/assets/javascripts/jobs/components/job_container_item.vue index 7bd299bcfa0..79e6623eca8 100644 --- a/app/assets/javascripts/jobs/components/job_container_item.vue +++ b/app/assets/javascripts/jobs/components/job_container_item.vue @@ -1,15 +1,14 @@ <script> -import { GlLink } from '@gitlab/ui'; +import { GlLink, GlIcon } from '@gitlab/ui'; import tooltip from '~/vue_shared/directives/tooltip'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; -import Icon from '~/vue_shared/components/icon.vue'; import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin'; import { sprintf } from '~/locale'; export default { components: { CiIcon, - Icon, + GlIcon, GlLink, }, directives: { @@ -56,7 +55,7 @@ export default { data-boundary="viewport" class="js-job-link d-flex" > - <icon + <gl-icon v-if="isActive" name="arrow-right" class="js-arrow-right icon-arrow-right position-absolute d-block" @@ -66,7 +65,7 @@ export default { <span class="text-truncate w-100">{{ job.name ? job.name : job.id }}</span> - <icon v-if="job.retried" name="retry" class="js-retry-icon" /> + <gl-icon v-if="job.retried" name="retry" class="js-retry-icon" /> </gl-link> </div> </template> diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue index 4d314eaa106..7e7d9a0549b 100644 --- a/app/assets/javascripts/jobs/components/job_log_controllers.vue +++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue @@ -1,16 +1,15 @@ <script> -import { GlTooltipDirective, GlLink, GlDeprecatedButton } from '@gitlab/ui'; +/* eslint-disable vue/no-v-html */ +import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui'; import { polyfillSticky } from '~/lib/utils/sticky'; -import Icon from '~/vue_shared/components/icon.vue'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import { __, sprintf } from '~/locale'; import scrollDown from '../svg/scroll_down.svg'; export default { components: { - Icon, GlLink, - GlDeprecatedButton, + GlButton, }, directives: { GlTooltip: GlTooltipDirective, @@ -87,18 +86,17 @@ export default { <div class="controllers float-right"> <!-- links --> - <gl-link + <gl-button v-if="rawPath" v-gl-tooltip.body :title="s__('Job|Show complete raw')" :href="rawPath" class="controllers-buttons" data-testid="job-raw-link-controller" - > - <icon name="doc-text" /> - </gl-link> + icon="doc-text" + /> - <gl-link + <gl-button v-if="erasePath" v-gl-tooltip.body :title="s__('Job|Erase job log')" @@ -107,30 +105,28 @@ export default { class="controllers-buttons" data-testid="job-log-erase-link" data-method="post" - > - <icon name="remove" /> - </gl-link> + icon="remove" + /> <!-- eo links --> <!-- scroll buttons --> <div v-gl-tooltip :title="s__('Job|Scroll to top')" class="controllers-buttons"> - <gl-deprecated-button + <gl-button :disabled="isScrollTopDisabled" - type="button" class="btn-scroll btn-transparent btn-blank" data-testid="job-controller-scroll-top" + icon="scroll_up" @click="handleScrollToTop" - > - <icon name="scroll_up" /> - </gl-deprecated-button> + /> </div> <div v-gl-tooltip :title="s__('Job|Scroll to bottom')" class="controllers-buttons"> - <gl-deprecated-button + <gl-button :disabled="isScrollBottomDisabled" class="js-scroll-bottom btn-scroll btn-transparent btn-blank" - :class="{ animate: isScrollingDown }" data-testid="job-controller-scroll-bottom" + icon="scroll_down" + :class="{ animate: isScrollingDown }" @click="handleScrollToBottom" v-html="$options.scrollDown" /> diff --git a/app/assets/javascripts/jobs/components/log/line.vue b/app/assets/javascripts/jobs/components/log/line.vue index 48f669ae8ed..e68d5b8eda4 100644 --- a/app/assets/javascripts/jobs/components/log/line.vue +++ b/app/assets/javascripts/jobs/components/log/line.vue @@ -20,7 +20,7 @@ export default { return h( 'span', { - class: ['ws-pre-wrap', content.style], + class: ['gl-white-space-pre-wrap', content.style], }, content.text, ); diff --git a/app/assets/javascripts/jobs/components/log/line_header.vue b/app/assets/javascripts/jobs/components/log/line_header.vue index 85ccd5996b5..4c1c00cb2a7 100644 --- a/app/assets/javascripts/jobs/components/log/line_header.vue +++ b/app/assets/javascripts/jobs/components/log/line_header.vue @@ -1,11 +1,11 @@ <script> -import Icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import LineNumber from './line_number.vue'; import DurationBadge from './duration_badge.vue'; export default { components: { - Icon, + GlIcon, LineNumber, DurationBadge, }, @@ -47,12 +47,12 @@ export default { role="button" @click="handleOnClick" > - <icon :name="iconName" class="arrow position-absolute" /> + <gl-icon :name="iconName" class="arrow position-absolute" /> <line-number :line-number="line.lineNumber" :path="path" /> <span v-for="(content, i) in line.content" :key="i" - class="line-text w-100 ws-pre-wrap" + class="line-text w-100 gl-white-space-pre-wrap" :class="content.style" >{{ content.text }}</span > diff --git a/app/assets/javascripts/jobs/components/manual_variables_form.vue b/app/assets/javascripts/jobs/components/manual_variables_form.vue index 9236624a191..bf1930c9a37 100644 --- a/app/assets/javascripts/jobs/components/manual_variables_form.vue +++ b/app/assets/javascripts/jobs/components/manual_variables_form.vue @@ -1,15 +1,14 @@ <script> +/* eslint-disable vue/no-v-html */ import { uniqueId } from 'lodash'; import { mapActions } from 'vuex'; -import { GlDeprecatedButton } from '@gitlab/ui'; +import { GlButton } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; export default { name: 'ManualVariablesForm', components: { - GlDeprecatedButton, - Icon, + GlButton, }, props: { action: { @@ -137,12 +136,12 @@ export default { <div class="table-section section-10"> <div class="table-mobile-header" role="rowheader"></div> <div class="table-mobile-content justify-content-end"> - <gl-deprecated-button - class="btn-transparent btn-blank w-25" + <gl-button + category="tertiary" + icon="clear" + :aria-label="__('Delete variable')" @click="deleteVariable(variable.id)" - > - <icon name="clear" /> - </gl-deprecated-button> + /> </div> </div> </div> @@ -176,9 +175,14 @@ export default { <p class="text-muted" v-html="helpText"></p> </div> <div class="d-flex justify-content-center"> - <gl-deprecated-button variant="primary" @click="triggerManualJob(variables)"> + <gl-button + variant="info" + category="primary" + :aria-label="__('Trigger manual job')" + @click="triggerManualJob(variables)" + > {{ action.button_title }} - </gl-deprecated-button> + </gl-button> </div> </div> </template> diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue index 517da16dcf8..aa589989e8a 100644 --- a/app/assets/javascripts/jobs/components/sidebar.vue +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -1,11 +1,11 @@ <script> import { isEmpty } from 'lodash'; import { mapActions, mapState } from 'vuex'; -import { GlLink, GlDeprecatedButton } from '@gitlab/ui'; +import { GlLink, GlDeprecatedButton, GlIcon } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import timeagoMixin from '~/vue_shared/mixins/timeago'; +import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; -import Icon from '~/vue_shared/components/icon.vue'; import DetailRow from './sidebar_detail_row.vue'; import ArtifactsBlock from './artifacts_block.vue'; import TriggerBlock from './trigger_block.vue'; @@ -19,15 +19,21 @@ export default { ArtifactsBlock, CommitBlock, DetailRow, - Icon, + GlIcon, TriggerBlock, StagesDropdown, JobsContainer, GlLink, GlDeprecatedButton, + TooltipOnTruncate, }, mixins: [timeagoMixin], props: { + artifactHelpUrl: { + type: String, + required: false, + default: '', + }, runnerHelpUrl: { type: String, required: false, @@ -112,7 +118,11 @@ export default { <div class="sidebar-container"> <div class="blocks-container"> <div class="block d-flex flex-nowrap align-items-center"> - <h4 class="my-0 mr-2 text-break-word">{{ job.name }}</h4> + <tooltip-on-truncate :title="job.name" truncate-target="child" + ><h4 class="my-0 mr-2 gl-text-truncate"> + {{ job.name }} + </h4> + </tooltip-on-truncate> <div class="flex-grow-1 flex-shrink-0 text-right"> <gl-link v-if="job.retry_path" @@ -157,7 +167,7 @@ export default { class="js-terminal-link btn btn-primary btn-inverted visible-md-block visible-lg-block float-left" target="_blank" > - {{ __('Debug') }} <icon name="external-link" :size="14" /> + {{ __('Debug') }} <gl-icon name="external-link" :size="14" /> </gl-link> </div> @@ -203,7 +213,7 @@ export default { </p> </div> - <artifacts-block v-if="hasArtifact" :artifact="job.artifact" /> + <artifacts-block v-if="hasArtifact" :artifact="job.artifact" :help-url="artifactHelpUrl" /> <trigger-block v-if="hasTriggers" :trigger="job.trigger" /> <commit-block :is-last-block="hasStages" diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue index b826007ec2c..7d541f93bad 100644 --- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue +++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue @@ -1,9 +1,10 @@ <script> -import { GlLink } from '@gitlab/ui'; +import { GlIcon, GlLink } from '@gitlab/ui'; export default { name: 'SidebarDetailRow', components: { + GlIcon, GlLink, }, props: { @@ -37,7 +38,7 @@ export default { <span v-if="hasTitle" class="font-weight-bold">{{ title }}:</span> {{ value }} <span v-if="hasHelpURL" class="help-button float-right"> <gl-link :href="helpUrl" target="_blank" rel="noopener noreferrer nofollow"> - <i class="fa fa-question-circle" aria-hidden="true"></i> + <gl-icon name="question-o" /> </gl-link> </span> </p> diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js index 024a13ce102..6e15360b66c 100644 --- a/app/assets/javascripts/jobs/index.js +++ b/app/assets/javascripts/jobs/index.js @@ -18,6 +18,7 @@ export default () => { }, render(createElement) { const { + artifactHelpUrl, deploymentHelpUrl, runnerHelpUrl, runnerSettingsUrl, @@ -32,6 +33,7 @@ export default () => { return createElement('job-app', { props: { + artifactHelpUrl, deploymentHelpUrl, runnerHelpUrl, runnerSettingsUrl, diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 1fb8e270e0e..8e172b4827c 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -12,6 +12,7 @@ import { deprecatedCreateFlash as flash } from './flash'; import ModalStore from './boards/stores/modal_store'; import boardsStore from './boards/stores/boards_store'; import { isScopedLabel } from '~/lib/utils/common_utils'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default class LabelsSelect { constructor(els, options = {}) { @@ -173,7 +174,7 @@ export default class LabelsSelect { }) .catch(() => flash(__('Error saving label update.'))); }; - $dropdown.glDropdown({ + initDeprecatedJQueryDropdown($dropdown, { showMenuAbove, data(term, callback) { const labelUrl = $dropdown.attr('data-labels'); @@ -203,7 +204,7 @@ export default class LabelsSelect { callback(data); if (showMenuAbove) { - $dropdown.data('glDropdown').positionMenuAbove(); + $dropdown.data('deprecatedJQueryDropdown').positionMenuAbove(); } }) .catch(() => flash(__('Error fetching labels.'))); @@ -348,7 +349,7 @@ export default class LabelsSelect { } else { if (!$dropdown.hasClass('js-filter-bulk-update')) { saveLabelData(); - $dropdown.data('glDropdown').clearMenu(); + $dropdown.data('deprecatedJQueryDropdown').clearMenu(); } } } @@ -455,7 +456,7 @@ export default class LabelsSelect { if ($dropdown.hasClass('js-issue-board-sidebar')) { const previousSelection = $dropdown.attr('data-selected'); this.selected = previousSelection ? previousSelection.split(',') : []; - $dropdown.data('glDropdown').updateLabel(); + $dropdown.data('deprecatedJQueryDropdown').updateLabel(); } }, preserveContext: true, diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index 0ecf3301250..d7f5e6f8a5e 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -1,7 +1,6 @@ import $ from 'jquery'; import ContextualSidebar from './contextual_sidebar'; import initFlyOutNav from './fly_out_nav'; -import initWhatsNew from '~/whats_new'; function hideEndFade($scrollingTabs) { $scrollingTabs.each(function scrollTabsLoop() { @@ -14,6 +13,17 @@ function hideEndFade($scrollingTabs) { function initDeferred() { $(document).trigger('init.scrolling-tabs'); + + const whatsNewTriggerEl = document.querySelector('.js-whats-new-trigger'); + if (whatsNewTriggerEl) { + whatsNewTriggerEl.addEventListener('click', () => { + import(/* webpackChunkName: 'whatsNewApp' */ '~/whats_new') + .then(({ default: initWhatsNew }) => { + initWhatsNew(); + }) + .catch(() => {}); + }); + } } export default function initLayoutNav() { @@ -21,7 +31,6 @@ export default function initLayoutNav() { contextualSidebar.bindEvents(); initFlyOutNav(); - initWhatsNew(); // We need to init it on DomContentLoaded as others could also call it $(document).on('init.scrolling-tabs', () => { diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js index 4fed121779e..d2907f401c0 100644 --- a/app/assets/javascripts/lib/graphql.js +++ b/app/assets/javascripts/lib/graphql.js @@ -4,6 +4,7 @@ import { createUploadLink } from 'apollo-upload-client'; import { ApolloLink } from 'apollo-link'; import { BatchHttpLink } from 'apollo-link-batch-http'; import csrf from '~/lib/utils/csrf'; +import PerformanceBarService from '~/performance_bar/services/performance_bar_service'; export const fetchPolicies = { CACHE_FIRST: 'cache-first', @@ -32,13 +33,35 @@ export default (resolvers = {}, config = {}) => { credentials: 'same-origin', }; + const uploadsLink = ApolloLink.split( + operation => operation.getContext().hasUpload || operation.getContext().isSingleRequest, + createUploadLink(httpOptions), + new BatchHttpLink(httpOptions), + ); + + const performanceBarLink = new ApolloLink((operation, forward) => { + return forward(operation).map(response => { + const httpResponse = operation.getContext().response; + + if (PerformanceBarService.interceptor) { + PerformanceBarService.interceptor({ + config: { + url: httpResponse.url, + }, + headers: { + 'x-request-id': httpResponse.headers.get('x-request-id'), + 'x-gitlab-from-cache': httpResponse.headers.get('x-gitlab-from-cache'), + }, + }); + } + + return response; + }); + }); + return new ApolloClient({ typeDefs: config.typeDefs, - link: ApolloLink.split( - operation => operation.getContext().hasUpload || operation.getContext().isSingleRequest, - createUploadLink(httpOptions), - new BatchHttpLink(httpOptions), - ), + link: ApolloLink.from([performanceBarLink, uploadsLink]), cache: new InMemoryCache({ ...config.cacheConfig, freezeResults: config.assumeImmutableResults, diff --git a/app/assets/javascripts/lib/utils/axios_startup_calls.js b/app/assets/javascripts/lib/utils/axios_startup_calls.js index a047cebc8ab..7e2665b910c 100644 --- a/app/assets/javascripts/lib/utils/axios_startup_calls.js +++ b/app/assets/javascripts/lib/utils/axios_startup_calls.js @@ -10,6 +10,32 @@ const getFullUrl = req => { return mergeUrlParams(req.params || {}, url); }; +const handleStartupCall = async ({ fetchCall }, req) => { + const res = await fetchCall; + if (!res.ok) { + throw new Error(res.statusText); + } + + const fetchHeaders = {}; + res.headers.forEach((val, key) => { + fetchHeaders[key] = val; + }); + + const data = await res.clone().json(); + + Object.assign(req, { + adapter: () => + Promise.resolve({ + data, + status: res.status, + statusText: res.statusText, + headers: fetchHeaders, + config: req, + request: req, + }), + }); +}; + const setupAxiosStartupCalls = axios => { const { startup_calls: startupCalls } = window.gl || {}; @@ -17,35 +43,28 @@ const setupAxiosStartupCalls = axios => { return; } - // TODO: To save performance of future axios calls, we can - // remove this interceptor once the "startupCalls" have been loaded - axios.interceptors.request.use(req => { + const remainingCalls = new Map(Object.entries(startupCalls)); + + const interceptor = axios.interceptors.request.use(async req => { const fullUrl = getFullUrl(req); - const existing = startupCalls[fullUrl]; - - if (existing) { - // eslint-disable-next-line no-param-reassign - req.adapter = () => - existing.fetchCall.then(res => { - const fetchHeaders = {}; - res.headers.forEach((val, key) => { - fetchHeaders[key] = val; - }); - - // eslint-disable-next-line promise/no-nesting - return res - .clone() - .json() - .then(data => ({ - data, - status: res.status, - statusText: res.statusText, - headers: fetchHeaders, - config: req, - request: req, - })); - }); + const startupCall = remainingCalls.get(fullUrl); + + if (!startupCall?.fetchCall) { + return req; + } + + try { + await handleStartupCall(startupCall, req); + } catch (e) { + // eslint-disable-next-line no-console + console.warn(`[gitlab] Something went wrong with the startup call for "${fullUrl}"`, e); + } + + remainingCalls.delete(fullUrl); + + if (remainingCalls.size === 0) { + axios.interceptors.request.eject(interceptor); } return req; diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index e26b63fbb85..b193a8b2c9a 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -216,8 +216,9 @@ export const timeFor = (time, expiredLabel) => { return timeago.format(time, `${timeagoLanguageCode}-remaining`).trim(); }; +export const millisecondsPerDay = 1000 * 60 * 60 * 24; + export const getDayDifference = (a, b) => { - const millisecondsPerDay = 1000 * 60 * 60 * 24; const date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate()); const date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate()); @@ -642,6 +643,16 @@ export const secondsToMilliseconds = seconds => seconds * 1000; export const secondsToDays = seconds => Math.round(seconds / 86400); /** + * Returns the date n days after the date provided + * + * @param {Date} date the initial date + * @param {Number} numberOfDays number of days after + * @return {Date} the date following the date provided + */ +export const nDaysAfter = (date, numberOfDays) => + new Date(newDate(date)).setDate(date.getDate() + numberOfDays); + +/** * Returns the date after the date provided * * @param {Date} date the initial date @@ -702,20 +713,14 @@ export const approximateDuration = (seconds = 0) => { * @return {Date} the date object from the params */ export const dateFromParams = (year, month, day) => { - const date = new Date(); - - date.setFullYear(year); - date.setMonth(month); - date.setDate(day); - - return date; + return new Date(year, month, day); }; /** * A utility function which computes the difference in seconds * between 2 dates. * - * @param {Date} startDate the start sate + * @param {Date} startDate the start date * @param {Date} endDate the end date * * @return {Int} the difference in seconds @@ -723,3 +728,18 @@ export const dateFromParams = (year, month, day) => { export const differenceInSeconds = (startDate, endDate) => { return (endDate.getTime() - startDate.getTime()) / 1000; }; + +/** + * A utility function which computes the difference in milliseconds + * between 2 dates. + * + * @param {Date|Int} startDate the start date. Can be either a date object or a unix timestamp. + * @param {Date|Int} endDate the end date. Can be either a date object or a unix timestamp. Defaults to now. + * + * @return {Int} the difference in milliseconds + */ +export const differenceInMilliseconds = (startDate, endDate = Date.now()) => { + const startDateInMS = startDate instanceof Date ? startDate.getTime() : startDate; + const endDateInMS = endDate instanceof Date ? endDate.getTime() : endDate; + return endDateInMS - startDateInMS; +}; diff --git a/app/assets/javascripts/lib/utils/forms.js b/app/assets/javascripts/lib/utils/forms.js index ced44ab9817..1c5f6cefeda 100644 --- a/app/assets/javascripts/lib/utils/forms.js +++ b/app/assets/javascripts/lib/utils/forms.js @@ -14,3 +14,40 @@ export const serializeForm = form => { return serializeFormEntries(entries); }; + +/** + * Check if the value provided is empty or not + * + * It is being used to check if a form input + * value has been set or not + * + * @param {String, Number, Array} - Any form value + * @returns {Boolean} - returns false if a value is set + * + * @example + * returns true for '', [], null, undefined + */ +export const isEmptyValue = value => value == null || value.length === 0; + +/** + * A form object serializer + * + * @param {Object} - Form Object + * @returns {Object} - Serialized Form Object + * + * @example + * Input + * {"project": {"value": "hello", "state": false}, "username": {"value": "john"}} + * + * Returns + * {"project": "hello", "username": "john"} + */ +export const serializeFormObject = form => + Object.fromEntries( + Object.entries(form).reduce((acc, [name, { value }]) => { + if (!isEmptyValue(value)) { + acc.push([name, value]); + } + return acc; + }, []), + ); diff --git a/app/assets/javascripts/lib/utils/image_utility.js b/app/assets/javascripts/lib/utils/image_utility.js index 2977ec821cb..af531f9f830 100644 --- a/app/assets/javascripts/lib/utils/image_utility.js +++ b/app/assets/javascripts/lib/utils/image_utility.js @@ -1,5 +1,3 @@ -/* eslint-disable import/prefer-default-export */ - export function isImageLoaded(element) { return element.complete && element.naturalHeight !== 0; } diff --git a/app/assets/javascripts/lib/utils/jquery_at_who.js b/app/assets/javascripts/lib/utils/jquery_at_who.js new file mode 100644 index 00000000000..88111cb4ae4 --- /dev/null +++ b/app/assets/javascripts/lib/utils/jquery_at_who.js @@ -0,0 +1,3 @@ +import 'jquery'; +import 'jquery.caret'; // required by at.js +import '@gitlab/at.js'; diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js index 7f0c65868c2..e8583fa951b 100644 --- a/app/assets/javascripts/lib/utils/poll.js +++ b/app/assets/javascripts/lib/utils/poll.js @@ -88,7 +88,11 @@ export default class Poll { } makeDelayedRequest(delay = 0) { - this.timeoutID = setTimeout(() => this.makeRequest(), delay); + // So we don't make our specs artificially slower + this.timeoutID = setTimeout( + () => this.makeRequest(), + process.env.NODE_ENV !== 'test' ? delay : 1, + ); } makeRequest() { diff --git a/app/assets/javascripts/lib/utils/set.js b/app/assets/javascripts/lib/utils/set.js index 3845d648b61..541934c4221 100644 --- a/app/assets/javascripts/lib/utils/set.js +++ b/app/assets/javascripts/lib/utils/set.js @@ -4,6 +4,5 @@ * @param {Set} superset The set to be considered as the superset. * @returns {boolean} */ -// eslint-disable-next-line import/prefer-default-export export const isSubset = (subset, superset) => 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 576a9ec880c..e4e9fb2e6fa 100644 --- a/app/assets/javascripts/lib/utils/simple_poll.js +++ b/app/assets/javascripts/lib/utils/simple_poll.js @@ -1,10 +1,12 @@ +import { differenceInMilliseconds } from '~/lib/utils/datetime_utility'; + 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 next = () => { - if (timeout === 0 || Date.now() - startTime < timeout) { + if (timeout === 0 || differenceInMilliseconds(startTime) < timeout) { setTimeout(fn.bind(null, next, stop), interval); } else { reject(new Error('SIMPLE_POLL_TIMEOUT')); diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 8d23d177410..f4c6e4e3584 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -1,6 +1,7 @@ /* eslint-disable func-names, no-param-reassign, operator-assignment, consistent-return */ import $ from 'jquery'; import { insertText } from '~/lib/utils/common_utils'; +import Shortcuts from '~/behaviors/shortcuts/shortcuts'; const LINK_TAG_PATTERN = '[{text}](url)'; @@ -303,23 +304,67 @@ function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagCo }); } +/* eslint-disable @gitlab/require-i18n-strings */ +export function keypressNoteText(e) { + if (this.selectionStart === this.selectionEnd) { + return; + } + const keys = { + '*': '**{text}**', // wraps with bold character + _: '_{text}_', // wraps with italic character + '`': '`{text}`', // wraps with inline character + "'": "'{text}'", // single quotes + '"': '"{text}"', // double quotes + '[': '[{text}]', // brackets + '{': '{{text}}', // braces + '(': '({text})', // parentheses + '<': '<{text}>', // angle brackets + }; + const tag = keys[e.key]; + + if (tag) { + e.preventDefault(); + + updateText({ + tag, + textArea: this, + blockTag: '', + wrap: true, + select: '', + tagContent: '', + }); + } +} +/* eslint-enable @gitlab/require-i18n-strings */ + +export function updateTextForToolbarBtn($toolbarBtn) { + return updateText({ + textArea: $toolbarBtn.closest('.md-area').find('textarea'), + tag: $toolbarBtn.data('mdTag'), + cursorOffset: $toolbarBtn.data('mdCursorOffset'), + blockTag: $toolbarBtn.data('mdBlock'), + wrap: !$toolbarBtn.data('mdPrepend'), + select: $toolbarBtn.data('mdSelect'), + tagContent: $toolbarBtn.data('mdTagContent'), + }); +} + export function addMarkdownListeners(form) { - return $('.js-md', form) + $('.markdown-area', form) + .on('keydown', keypressNoteText) + .each(function attachTextareaShortcutHandlers() { + Shortcuts.initMarkdownEditorShortcuts($(this), updateTextForToolbarBtn); + }); + + const $allToolbarBtns = $('.js-md', form) .off('click') .on('click', function() { - const $this = $(this); - const tag = this.dataset.mdTag; - - return updateText({ - textArea: $this.closest('.md-area').find('textarea'), - tag, - cursorOffset: $this.data('mdCursorOffset'), - blockTag: $this.data('mdBlock'), - wrap: !$this.data('mdPrepend'), - select: $this.data('mdSelect'), - tagContent: $this.data('mdTagContent'), - }); + const $toolbarBtn = $(this); + + return updateTextForToolbarBtn($toolbarBtn); }); + + return $allToolbarBtns; } export function addEditorMarkdownListeners(editor) { @@ -342,5 +387,11 @@ export function addEditorMarkdownListeners(editor) { } export function removeMarkdownListeners(form) { + $('.markdown-area', form) + .off('keydown', keypressNoteText) + .each(function removeTextareaShortcutHandlers() { + Shortcuts.removeMarkdownEditorShortcuts($(this)); + }); + return $('.js-md', form).off('click'); } diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index e2953ce330c..8ac6a44cba9 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -275,6 +275,81 @@ export const convertToSentenceCase = string => { */ export const convertToTitleCase = string => string.replace(/\b[a-z]/g, s => s.toUpperCase()); +const unicodeConversion = [ + [/[ÀÁÂÃÅĀĂĄ]/g, 'A'], + [/[Æ]/g, 'AE'], + [/[ÇĆĈĊČ]/g, 'C'], + [/[ÈÉÊËĒĔĖĘĚ]/g, 'E'], + [/[ÌÍÎÏĨĪĬĮİ]/g, 'I'], + [/[Ððĥħ]/g, 'h'], + [/[ÑŃŅŇʼn]/g, 'N'], + [/[ÒÓÔÕØŌŎŐ]/g, 'O'], + [/[ÙÚÛŨŪŬŮŰŲ]/g, 'U'], + [/[ÝŶŸ]/g, 'Y'], + [/[Þñþńņň]/g, 'n'], + [/[ߌŜŞŠ]/g, 'S'], + [/[àáâãåāăąĸ]/g, 'a'], + [/[æ]/g, 'ae'], + [/[çćĉċč]/g, 'c'], + [/[èéêëēĕėęě]/g, 'e'], + [/[ìíîïĩīĭį]/g, 'i'], + [/[òóôõøōŏő]/g, 'o'], + [/[ùúûũūŭůűų]/g, 'u'], + [/[ýÿŷ]/g, 'y'], + [/[ĎĐ]/g, 'D'], + [/[ďđ]/g, 'd'], + [/[ĜĞĠĢ]/g, 'G'], + [/[ĝğġģŊŋſ]/g, 'g'], + [/[ĤĦ]/g, 'H'], + [/[ıśŝşš]/g, 's'], + [/[IJ]/g, 'IJ'], + [/[ij]/g, 'ij'], + [/[Ĵ]/g, 'J'], + [/[ĵ]/g, 'j'], + [/[Ķ]/g, 'K'], + [/[ķ]/g, 'k'], + [/[ĹĻĽĿŁ]/g, 'L'], + [/[ĺļľŀł]/g, 'l'], + [/[Œ]/g, 'OE'], + [/[œ]/g, 'oe'], + [/[ŔŖŘ]/g, 'R'], + [/[ŕŗř]/g, 'r'], + [/[ŢŤŦ]/g, 'T'], + [/[ţťŧ]/g, 't'], + [/[Ŵ]/g, 'W'], + [/[ŵ]/g, 'w'], + [/[ŹŻŽ]/g, 'Z'], + [/[źżž]/g, 'z'], + [/ö/g, 'oe'], + [/ü/g, 'ue'], + [/ä/g, 'ae'], + // eslint-disable-next-line @gitlab/require-i18n-strings + [/Ö/g, 'Oe'], + // eslint-disable-next-line @gitlab/require-i18n-strings + [/Ü/g, 'Ue'], + // eslint-disable-next-line @gitlab/require-i18n-strings + [/Ä/g, 'Ae'], +]; + +/** + * Converts each non-ascii character in a string to + * it's ascii equivalent (if defined). + * + * e.g. "Dĭd söméònê äšk fœŕ Ůnĭċődę?" => "Did someone aesk foer Unicode?" + * + * @param {String} string + * @returns {String} + */ +export const convertUnicodeToAscii = string => { + let convertedString = string; + + unicodeConversion.forEach(([regex, replacer]) => { + convertedString = convertedString.replace(regex, replacer); + }); + + return convertedString; +}; + /** * Splits camelCase or PascalCase words * e.g. HelloWorld => Hello World diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js index be86f336bcd..664c0dbbc84 100644 --- a/app/assets/javascripts/lib/utils/type_utility.js +++ b/app/assets/javascripts/lib/utils/type_utility.js @@ -1,2 +1 @@ -// eslint-disable-next-line import/prefer-default-export export const isObject = obj => obj && obj.constructor === Object; diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 8077570158a..e9c3fe0a406 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -75,7 +75,7 @@ export function getParameterValues(sParam, url = window.location) { * @param {Boolean} options.spreadArrays - split array values into separate key/value-pairs */ export function mergeUrlParams(params, url, options = {}) { - const { spreadArrays = false } = options; + const { spreadArrays = false, sort = false } = options; const re = /^([^?#]*)(\?[^#]*)?(.*)/; let merged = {}; const [, fullpath, query, fragment] = url.match(re); @@ -108,7 +108,9 @@ export function mergeUrlParams(params, url, options = {}) { Object.assign(merged, params); - const newQuery = Object.keys(merged) + const mergedKeys = sort ? Object.keys(merged).sort() : Object.keys(merged); + + const newQuery = mergedKeys .filter(key => merged[key] !== null) .map(key => { let value = merged[key]; @@ -334,17 +336,32 @@ export function getWebSocketUrl(path) { * Convert search query into an object * * @param {String} query from "document.location.search" + * @param {Object} options + * @param {Boolean} options.gatherArrays - gather array values into an Array * @returns {Object} * * ex: "?one=1&two=2" into {one: 1, two: 2} */ -export function queryToObject(query) { +export function queryToObject(query, options = {}) { + const { gatherArrays = false } = options; const removeQuestionMarkFromQuery = String(query).startsWith('?') ? query.slice(1) : query; return removeQuestionMarkFromQuery.split('&').reduce((accumulator, curr) => { const [key, value] = curr.split('='); - if (value !== undefined) { - accumulator[decodeURIComponent(key)] = decodeURIComponent(value); + if (value === undefined) { + return accumulator; } + const decodedValue = decodeURIComponent(value); + + if (gatherArrays && key.endsWith('[]')) { + const decodedKey = decodeURIComponent(key.slice(0, -2)); + if (!Array.isArray(accumulator[decodedKey])) { + accumulator[decodedKey] = []; + } + accumulator[decodedKey].push(decodedValue); + } else { + accumulator[decodeURIComponent(key)] = decodedValue; + } + return accumulator; }, {}); } diff --git a/app/assets/javascripts/lib/utils/webpack.js b/app/assets/javascripts/lib/utils/webpack.js index 390294afcb7..622c40e0f35 100644 --- a/app/assets/javascripts/lib/utils/webpack.js +++ b/app/assets/javascripts/lib/utils/webpack.js @@ -1,7 +1,6 @@ import { joinPaths } from '~/lib/utils/url_utility'; // tell webpack to load assets from origin so that web workers don't break -// eslint-disable-next-line import/prefer-default-export export function resetServiceWorkersPublicPath() { // __webpack_public_path__ is a global variable that can be used to adjust // the webpack publicPath setting at runtime. diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index ed10c7646a8..8621a133776 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -1,6 +1,7 @@ /* eslint-disable func-names, no-underscore-dangle, no-param-reassign, consistent-return */ import $ from 'jquery'; +import 'vendor/jquery.scrollTo'; // LineHighlighter // diff --git a/app/assets/javascripts/logs/stores/getters.js b/app/assets/javascripts/logs/stores/getters.js index d92969c5389..dc392af8381 100644 --- a/app/assets/javascripts/logs/stores/getters.js +++ b/app/assets/javascripts/logs/stores/getters.js @@ -6,8 +6,16 @@ const mapTrace = ({ timestamp = null, pod = '', message = '' }) => export const trace = state => state.logs.lines.map(mapTrace).join('\n'); export const showAdvancedFilters = state => { - const environment = state.environments.options.find( - ({ name }) => name === state.environments.current, + if (state.environments.current) { + const environment = state.environments.options.find( + ({ name }) => name === state.environments.current, + ); + + return Boolean(environment?.enable_advanced_logs_querying); + } + const managedApp = state.managedApps.options.find( + ({ name }) => name === state.managedApps.current, ); - return Boolean(environment?.enable_advanced_logs_querying); + + return Boolean(managedApp?.enable_advanced_logs_querying); }; diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 1572e82a66c..9fcf881a1ac 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -20,25 +20,22 @@ import { localTimeAgo } from './lib/utils/datetime_utility'; import { getLocationHash, visitUrl } from './lib/utils/url_utility'; // everything else -import loadAwardsHandler from './awards_handler'; import { deprecatedCreateFlash as Flash, removeFlashClickListener } from './flash'; -import './gl_dropdown'; import initTodoToggle from './header'; import initImporterStatus from './importer_status'; import initLayoutNav from './layout_nav'; +import initAlertHandler from './alert_handler'; import './feature_highlight/feature_highlight_options'; import LazyLoader from './lazy_loader'; import initLogoAnimation from './logo'; import initFrequentItemDropdowns from './frequent_items'; import initBreadcrumbs from './breadcrumb'; import initUsagePingConsent from './usage_ping_consent'; -import initPerformanceBar from './performance_bar'; -import initSearchAutocomplete from './search_autocomplete'; import GlFieldErrors from './gl_field_errors'; import initUserPopovers from './user_popovers'; import initBroadcastNotifications from './broadcast_notification'; import initPersistentUserCallouts from './persistent_user_callouts'; -import { initUserTracking } from './tracking'; +import { initUserTracking, initDefaultTrackers } from './tracking'; import { __ } from './locale'; import 'ee_else_ce/main_ee'; @@ -112,8 +109,23 @@ function deferredInitialisation() { initBroadcastNotifications(); initFrequentItemDropdowns(); initPersistentUserCallouts(); - - if (document.querySelector('.search')) initSearchAutocomplete(); + initDefaultTrackers(); + + const search = document.querySelector('#search'); + if (search) { + search.addEventListener( + 'focus', + () => { + import(/* webpackChunkName: 'globalSearch' */ './search_autocomplete') + .then(({ default: initSearchAutocomplete }) => { + const searchDropdown = initSearchAutocomplete(); + searchDropdown.onSearchInputFocus(); + }) + .catch(() => {}); + }, + { once: true }, + ); + } addSelectOnFocusBehaviour('.js-select-on-focus'); @@ -155,8 +167,6 @@ function deferredInitialisation() { viewport: '.layout-page', }); - loadAwardsHandler(); - // Adding a helper class to activate animations only after all is rendered setTimeout(() => $body.addClass('page-initialised'), 1000); } @@ -166,10 +176,9 @@ document.addEventListener('DOMContentLoaded', () => { const $document = $(document); const bootstrapBreakpoint = bp.getBreakpointSize(); - if (document.querySelector('#js-peek')) initPerformanceBar({ container: '#js-peek' }); - initUserTracking(); initLayoutNav(); + initAlertHandler(); // Set the default path for all cookies to GitLab's root directory Cookies.defaults.path = gon.relative_url_root || '/'; diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js index f220e9e0192..c3fbb5d6acf 100644 --- a/app/assets/javascripts/members.js +++ b/app/assets/javascripts/members.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import { disableButtonIfEmptyField } from '~/lib/utils/common_utils'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default class Members { constructor() { @@ -37,7 +38,7 @@ export default class Members { $('.js-member-permissions-dropdown').each((i, btn) => { const $btn = $(btn); - $btn.glDropdown({ + initDeprecatedJQueryDropdown($btn, { selectable: true, isSelectable: (selected, $el) => this.dropdownIsSelectable(selected, $el), fieldName: $btn.data('fieldName'), diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 8322d36faee..79a4c3700ef 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -66,6 +66,14 @@ MergeRequest.prototype.showAllCommits = function() { MergeRequest.prototype.initMRBtnListeners = function() { const _this = this; + + $('.report-abuse-link').on('click', e => { + // this is needed because of the implementation of + // the dropdown toggle and Report Abuse needing to be + // linked to another page. + e.stopPropagation(); + }); + return $('.btn-close, .btn-reopen').on('click', function(e) { const $this = $(this); const shouldSubmit = $this.hasClass('btn-comment'); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 94b6ba7b1ce..b7cf39db00c 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -1,6 +1,7 @@ /* 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'; diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index caa45184bfc..52f6786ca28 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -4,10 +4,11 @@ import $ from 'jquery'; import { template, escape } from 'lodash'; -import { __ } from '~/locale'; -import '~/gl_dropdown'; +import { __, sprintf } from '~/locale'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; +import Api from '~/api'; import axios from './lib/utils/axios_utils'; -import { timeFor } from './lib/utils/datetime_utility'; +import { timeFor, parsePikadayDate, dateInWords } from './lib/utils/datetime_utility'; import ModalStore from './boards/stores/modal_store'; import boardsStore, { boardStoreIssueSet, @@ -34,10 +35,10 @@ export default class MilestoneSelect { $els.each((i, dropdown) => { let milestoneLinkNoneTemplate, milestoneLinkTemplate, + milestoneExpiredLinkTemplate, selectedMilestone, selectedMilestoneDefault; const $dropdown = $(dropdown); - const milestonesUrl = $dropdown.data('milestones'); const issueUpdateURL = $dropdown.data('issueUpdate'); const showNo = $dropdown.data('showNo'); const showAny = $dropdown.data('showAny'); @@ -63,59 +64,109 @@ export default class MilestoneSelect { milestoneLinkTemplate = template( '<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>', ); + milestoneExpiredLinkTemplate = template( + '<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %> (Past due)</a>', + ); milestoneLinkNoneTemplate = `<span class="no-value">${__('None')}</span>`; } - return $dropdown.glDropdown({ + return initDeprecatedJQueryDropdown($dropdown, { showMenuAbove, - data: (term, callback) => - axios.get(milestonesUrl).then(({ data }) => { - const extraOptions = []; - if (showAny) { - extraOptions.push({ - id: null, - name: null, - title: __('Any milestone'), - }); - } - if (showNo) { - extraOptions.push({ - id: -1, - name: __('No milestone'), - title: __('No milestone'), - }); - } - if (showUpcoming) { - extraOptions.push({ - id: -2, - name: '#upcoming', - title: __('Upcoming'), - }); - } - if (showStarted) { - extraOptions.push({ - id: -3, - name: '#started', - title: __('Started'), - }); - } - if (extraOptions.length) { - extraOptions.push({ type: 'divider' }); - } + data: (term, callback) => { + let contextId = parseInt($dropdown.get(0).dataset.projectId, 10); + let getMilestones = Api.projectMilestones.bind(Api); + const reqParams = { state: 'active', include_parent_milestones: true }; - callback(extraOptions.concat(data)); - if (showMenuAbove) { - $dropdown.data('glDropdown').positionMenuAbove(); - } - $(`[data-milestone-id="${escape(selectedMilestone)}"] > a`).addClass('is-active'); - }), - renderRow: milestone => ` - <li data-milestone-id="${escape(milestone.name)}"> + if (term) { + reqParams.search = term.trim(); + } + + if (!contextId) { + contextId = $dropdown.get(0).dataset.groupId; + delete reqParams.include_parent_milestones; + getMilestones = Api.groupMilestones.bind(Api); + } + + // We don't use $.data() as it caches initial value and never updates! + return getMilestones(contextId, reqParams) + .then(({ data }) => + data + .map(m => ({ + ...m, + // Public API includes `title` instead of `name`. + name: m.title, + })) + .sort((mA, mB) => { + // Move all expired milestones to the bottom. + if (mA.expired) { + return 1; + } + if (mB.expired) { + return -1; + } + return 0; + }), + ) + .then(data => { + const extraOptions = []; + if (showAny) { + extraOptions.push({ + id: null, + name: null, + title: __('Any milestone'), + }); + } + if (showNo) { + extraOptions.push({ + id: -1, + name: __('No milestone'), + title: __('No milestone'), + }); + } + if (showUpcoming) { + extraOptions.push({ + id: -2, + name: '#upcoming', + title: __('Upcoming'), + }); + } + if (showStarted) { + extraOptions.push({ + id: -3, + name: '#started', + title: __('Started'), + }); + } + if (extraOptions.length) { + extraOptions.push({ type: 'divider' }); + } + + callback(extraOptions.concat(data)); + if (showMenuAbove) { + $dropdown.data('deprecatedJQueryDropdown').positionMenuAbove(); + } + $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active'); + }); + }, + renderRow: milestone => { + const milestoneName = milestone.title || milestone.name; + let milestoneDisplayName = escape(milestoneName); + + if (milestone.expired) { + milestoneDisplayName = sprintf(__('%{milestone} (expired)'), { + milestone: milestoneDisplayName, + }); + } + + return ` + <li data-milestone-id="${escape(milestoneName)}"> <a href='#' class='dropdown-menu-milestone-link'> - ${escape(milestone.title)} + ${milestoneDisplayName} </a> </li> - `, + `; + }, filterable: true, + filterRemote: true, search: { fields: ['title'], }, @@ -149,7 +200,7 @@ export default class MilestoneSelect { selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault; } $('a.is-active', $el).removeClass('is-active'); - $(`[data-milestone-id="${escape(selectedMilestone)}"] > a`, $el).addClass('is-active'); + $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active'); }, vue: $dropdown.hasClass('js-issue-board-sidebar'), clicked: clickEvent => { @@ -237,7 +288,16 @@ export default class MilestoneSelect { if (data.milestone != null) { data.milestone.remaining = timeFor(data.milestone.due_date); data.milestone.name = data.milestone.title; - $value.html(milestoneLinkTemplate(data.milestone)); + $value.html( + data.milestone.expired + ? milestoneExpiredLinkTemplate({ + ...data.milestone, + remaining: sprintf(__('%{due_date} (Past due)'), { + due_date: dateInWords(parsePikadayDate(data.milestone.due_date)), + }), + }) + : milestoneLinkTemplate(data.milestone), + ); return $sidebarCollapsedValue .attr( 'data-original-title', diff --git a/app/assets/javascripts/milestones/project_milestone_combobox.vue b/app/assets/javascripts/milestones/project_milestone_combobox.vue index d0179ab5509..5ee917573ce 100644 --- a/app/assets/javascripts/milestones/project_milestone_combobox.vue +++ b/app/assets/javascripts/milestones/project_milestone_combobox.vue @@ -1,9 +1,9 @@ <script> import { - GlNewDropdown, - GlNewDropdownDivider, - GlNewDropdownHeader, - GlNewDropdownItem, + GlDropdown, + GlDropdownDivider, + GlDropdownSectionHeader, + GlDropdownItem, GlLoadingIcon, GlSearchBoxByType, GlIcon, @@ -13,12 +13,14 @@ import { __, sprintf } from '~/locale'; import Api from '~/api'; import { deprecatedCreateFlash as createFlash } from '~/flash'; +const SEARCH_DEBOUNCE_MS = 250; + export default { components: { - GlNewDropdown, - GlNewDropdownDivider, - GlNewDropdownHeader, - GlNewDropdownItem, + GlDropdown, + GlDropdownDivider, + GlDropdownSectionHeader, + GlDropdownItem, GlLoadingIcon, GlSearchBoxByType, GlIcon, @@ -89,10 +91,21 @@ export default { return this.requestCount !== 0; }, }, + created() { + // This method is defined here instead of in `methods` + // because we need to access the .cancel() method + // lodash attaches to the function, which is + // made inaccessible by Vue. More info: + // https://stackoverflow.com/a/52988020/1063392 + this.debouncedSearchMilestones = debounce(this.searchMilestones, SEARCH_DEBOUNCE_MS); + }, mounted() { this.fetchMilestones(); }, methods: { + focusSearchBox() { + this.$refs.searchBox.$el.querySelector('input').focus(); + }, fetchMilestones() { this.requestCount += 1; @@ -108,7 +121,7 @@ export default { this.requestCount -= 1; }); }, - searchMilestones: debounce(function searchMilestones() { + searchMilestones() { this.requestCount += 1; const options = { search: this.searchQuery, @@ -133,7 +146,14 @@ export default { .finally(() => { this.requestCount -= 1; }); - }, 100), + }, + onSearchBoxInput() { + this.debouncedSearchMilestones(); + }, + onSearchBoxEnter() { + this.debouncedSearchMilestones.cancel(); + this.searchMilestones(); + }, toggleMilestoneSelection(clickedMilestone) { if (!clickedMilestone) return []; @@ -168,7 +188,7 @@ export default { </script> <template> - <gl-new-dropdown> + <gl-dropdown v-bind="$attrs" class="project-milestone-combobox" @shown="focusSearchBox"> <template slot="button-content"> <span ref="buttonText" class="flex-grow-1 ml-1 text-muted">{{ selectedMilestonesLabel @@ -176,39 +196,41 @@ export default { <gl-icon name="chevron-down" /> </template> - <gl-new-dropdown-header> + <gl-dropdown-section-header> <span class="text-center d-block">{{ $options.translations.selectMilestone }}</span> - </gl-new-dropdown-header> + </gl-dropdown-section-header> - <gl-new-dropdown-divider /> + <gl-dropdown-divider /> <gl-search-box-by-type + ref="searchBox" v-model.trim="searchQuery" - class="m-2" + class="gl-m-3" :placeholder="this.$options.translations.searchMilestones" - @input="searchMilestones" + @input="onSearchBoxInput" + @keydown.enter.prevent="onSearchBoxEnter" /> - <gl-new-dropdown-item @click="onMilestoneClicked(null)"> + <gl-dropdown-item @click="onMilestoneClicked(null)"> <span :class="{ 'pl-4': true, 'selected-item': selectedMilestones.length === 0 }"> {{ $options.translations.noMilestone }} </span> - </gl-new-dropdown-item> + </gl-dropdown-item> - <gl-new-dropdown-divider /> + <gl-dropdown-divider /> <template v-if="isLoading"> <gl-loading-icon /> - <gl-new-dropdown-divider /> + <gl-dropdown-divider /> </template> <template v-else-if="noResults"> <div class="dropdown-item-space"> <span ref="noResults" class="pl-4">{{ $options.translations.noResultsLabel }}</span> </div> - <gl-new-dropdown-divider /> + <gl-dropdown-divider /> </template> <template v-else-if="dropdownItems.length"> - <gl-new-dropdown-item + <gl-dropdown-item v-for="item in dropdownItems" :key="item" role="milestone option" @@ -217,12 +239,12 @@ export default { <span :class="{ 'pl-4': true, 'selected-item': isSelectedMilestone(item) }"> {{ item }} </span> - </gl-new-dropdown-item> - <gl-new-dropdown-divider /> + </gl-dropdown-item> + <gl-dropdown-divider /> </template> - <gl-new-dropdown-item v-for="(item, idx) in extraLinks" :key="idx" :href="item.url"> + <gl-dropdown-item v-for="(item, idx) in extraLinks" :key="idx" :href="item.url"> <span class="pl-4">{{ item.text }}</span> - </gl-new-dropdown-item> - </gl-new-dropdown> + </gl-dropdown-item> + </gl-dropdown> </template> diff --git a/app/assets/javascripts/monitoring/components/alert_widget.vue b/app/assets/javascripts/monitoring/components/alert_widget.vue index 909ae2980d2..8f9c181258f 100644 --- a/app/assets/javascripts/monitoring/components/alert_widget.vue +++ b/app/assets/javascripts/monitoring/components/alert_widget.vue @@ -236,7 +236,7 @@ export default { > <gl-badge :variant="isFiring ? 'danger' : 'neutral'" class="d-flex-center text-truncate"> <gl-icon name="warning" :size="16" class="flex-shrink-0" /> - <span class="text-truncate gl-pl-1-deprecated-no-really-do-not-use-me"> + <span class="text-truncate gl-pl-2"> <gl-sprintf :message=" hasMultipleAlerts ? multipleAlertsSummary.message : singleAlertSummary.message diff --git a/app/assets/javascripts/monitoring/components/alert_widget_form.vue b/app/assets/javascripts/monitoring/components/alert_widget_form.vue index 5fa0da53a04..132df9c9516 100644 --- a/app/assets/javascripts/monitoring/components/alert_widget_form.vue +++ b/app/assets/javascripts/monitoring/components/alert_widget_form.vue @@ -7,16 +7,16 @@ import { GlButtonGroup, GlFormGroup, GlFormInput, - GlNewDropdown as GlDropdown, - GlNewDropdownItem as GlDropdownItem, + GlDropdown, + GlDropdownItem, GlModal, GlTooltipDirective, + GlIcon, } from '@gitlab/ui'; import { __, s__ } from '~/locale'; import Translate from '~/vue_shared/translate'; import TrackEventDirective from '~/vue_shared/directives/track_event'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import Icon from '~/vue_shared/components/icon.vue'; import { alertsValidator, queriesValidator } from '../validators'; import { OPERATORS } from '../constants'; @@ -44,7 +44,7 @@ export default { GlDropdownItem, GlModal, GlLink, - Icon, + GlIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -242,7 +242,7 @@ export default { <template #description> <div class="d-flex align-items-center"> {{ __('Single or combined queries') }} - <icon + <gl-icon v-gl-tooltip="$options.alertQueryText.descriptionTooltip" name="question" class="gl-ml-2" diff --git a/app/assets/javascripts/monitoring/components/charts/empty_chart.vue b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue index ad176637538..446ca8e5090 100644 --- a/app/assets/javascripts/monitoring/components/charts/empty_chart.vue +++ b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import chartEmptyStateIllustration from '@gitlab/svgs/dist/illustrations/chart-empty-state.svg'; import { chartHeight } from '../../constants'; diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue index 054111c203e..6bae3fdcc2e 100644 --- a/app/assets/javascripts/monitoring/components/charts/time_series.vue +++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue @@ -1,10 +1,9 @@ <script> import { isEmpty, omit, throttle } from 'lodash'; -import { GlLink, GlTooltip, GlResizeObserverDirective } from '@gitlab/ui'; +import { GlLink, GlTooltip, GlResizeObserverDirective, GlIcon } from '@gitlab/ui'; import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; import { s__ } from '~/locale'; import { getSvgIconPathContent } from '~/lib/utils/icon_utils'; -import Icon from '~/vue_shared/components/icon.vue'; import { panelTypes, chartHeight, lineTypes, lineWidths, legendLayoutTypes } from '../../constants'; import { getYAxisOptions, getTimeAxisOptions, getChartGrid, getTooltipFormatter } from './options'; import { annotationsYAxis, generateAnnotationsSeries } from './annotations'; @@ -27,7 +26,7 @@ export default { GlTooltip, GlChartSeriesLabel, GlLink, - Icon, + GlIcon, }, directives: { GlResizeObserverDirective, @@ -407,7 +406,7 @@ export default { {{ __('Deployed') }} </template> <div slot="tooltipContent" class="d-flex align-items-center"> - <icon name="commit" class="mr-2" /> + <gl-icon name="commit" class="mr-2" /> <gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link> </div> </template> diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 24aa7b3f504..cbfacd73b5b 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -2,7 +2,7 @@ import { mapActions, mapState, mapGetters } from 'vuex'; import VueDraggable from 'vuedraggable'; import Mousetrap from 'mousetrap'; -import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui'; +import { GlButton, GlModalDirective, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import DashboardHeader from './dashboard_header.vue'; import DashboardPanel from './dashboard_panel.vue'; import { s__ } from '~/locale'; @@ -10,7 +10,6 @@ import { deprecatedCreateFlash as createFlash } from '~/flash'; import { ESC_KEY } from '~/lib/utils/keys'; import { mergeUrlParams, updateHistory } from '~/lib/utils/url_utility'; import invalidUrl from '~/lib/utils/invalid_url'; -import Icon from '~/vue_shared/components/icon.vue'; import GraphGroup from './graph_group.vue'; import EmptyState from './empty_state.vue'; @@ -33,7 +32,7 @@ export default { VueDraggable, DashboardHeader, DashboardPanel, - Icon, + GlIcon, GlButton, GraphGroup, EmptyState, @@ -473,7 +472,7 @@ export default { @click="removePanel(groupData.key, groupData.panels, graphIndex)" > <a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')"> - <icon name="close" /> + <gl-icon name="close" /> </a> </div> diff --git a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue index 68afa2ace01..070277fe2dc 100644 --- a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue +++ b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue @@ -2,9 +2,9 @@ import { mapState, mapGetters, mapActions } from 'vuex'; import { GlDeprecatedButton, - GlNewDropdown, - GlNewDropdownDivider, - GlNewDropdownItem, + GlDropdown, + GlDropdownDivider, + GlDropdownItem, GlModal, GlIcon, GlModalDirective, @@ -23,9 +23,9 @@ import { getAddMetricTrackingOptions } from '../utils'; export default { components: { GlDeprecatedButton, - GlNewDropdown, - GlNewDropdownDivider, - GlNewDropdownItem, + GlDropdown, + GlDropdownDivider, + GlDropdownItem, GlModal, GlIcon, DuplicateDashboardModal, @@ -143,7 +143,7 @@ export default { as part of https://gitlab.com/gitlab-org/gitlab-ui/-/issues/936 The variant will create a dropdown with an icon, no text and no caret --> - <gl-new-dropdown + <gl-dropdown v-gl-tooltip data-testid="actions-menu" data-qa-selector="actions_menu_dropdown" @@ -157,13 +157,13 @@ export default { </template> <template v-if="addingMetricsAvailable"> - <gl-new-dropdown-item + <gl-dropdown-item v-gl-modal="$options.modalIds.addMetric" data-qa-selector="add_metric_button" data-testid="add-metric-item" > {{ $options.i18n.addMetric }} - </gl-new-dropdown-item> + </gl-dropdown-item> <gl-modal ref="addMetricModal" :modal-id="$options.modalIds.addMetric" @@ -194,20 +194,20 @@ export default { </gl-modal> </template> - <gl-new-dropdown-item + <gl-dropdown-item v-if="isMenuItemEnabled.addPanel" data-testid="add-panel-item-enabled" :to="newPanelPageLocation" > {{ $options.i18n.addPanel }} - </gl-new-dropdown-item> + </gl-dropdown-item> <!-- wrapper for tooltip as button can be `disabled` https://bootstrap-vue.org/docs/components/tooltip#disabled-elements --> <div v-else v-gl-tooltip :title="$options.i18n.addPanelInfo"> - <gl-new-dropdown-item + <gl-dropdown-item :alt="$options.i18n.addPanelInfo" :to="newPanelPageLocation" data-testid="add-panel-item-disabled" @@ -215,24 +215,24 @@ export default { class="gl-cursor-not-allowed" > <span class="gl-text-gray-400">{{ $options.i18n.addPanel }}</span> - </gl-new-dropdown-item> + </gl-dropdown-item> </div> - <gl-new-dropdown-item + <gl-dropdown-item v-if="isMenuItemEnabled.editDashboard" :href="selectedDashboard ? selectedDashboard.project_blob_path : null" data-qa-selector="edit_dashboard_button_enabled" data-testid="edit-dashboard-item-enabled" > {{ $options.i18n.editDashboard }} - </gl-new-dropdown-item> + </gl-dropdown-item> <!-- wrapper for tooltip as button can be `disabled` https://bootstrap-vue.org/docs/components/tooltip#disabled-elements --> <div v-else v-gl-tooltip :title="$options.i18n.editDashboardInfo"> - <gl-new-dropdown-item + <gl-dropdown-item :alt="$options.i18n.editDashboardInfo" :href="selectedDashboard ? selectedDashboard.project_blob_path : null" data-testid="edit-dashboard-item-disabled" @@ -240,16 +240,16 @@ export default { class="gl-cursor-not-allowed" > <span class="gl-text-gray-400">{{ $options.i18n.editDashboard }}</span> - </gl-new-dropdown-item> + </gl-dropdown-item> </div> <template v-if="isMenuItemShown.duplicateDashboard"> - <gl-new-dropdown-item + <gl-dropdown-item v-gl-modal="$options.modalIds.duplicateDashboard" data-testid="duplicate-dashboard-item" > {{ $options.i18n.duplicateDashboard }} - </gl-new-dropdown-item> + </gl-dropdown-item> <duplicate-dashboard-modal :default-branch="defaultBranch" @@ -259,25 +259,25 @@ export default { /> </template> - <gl-new-dropdown-item + <gl-dropdown-item v-if="selectedDashboard" data-testid="star-dashboard-item" :disabled="isUpdatingStarredValue" @click="toggleStarredValue()" > {{ selectedDashboard.starred ? $options.i18n.unstarDashboard : $options.i18n.starDashboard }} - </gl-new-dropdown-item> + </gl-dropdown-item> - <gl-new-dropdown-divider /> + <gl-dropdown-divider /> - <gl-new-dropdown-item + <gl-dropdown-item v-gl-modal="$options.modalIds.createDashboard" data-testid="create-dashboard-item" :disabled="!isMenuItemEnabled.createDashboard" :class="{ 'monitoring-actions-item-disabled': !isMenuItemEnabled.createDashboard }" > {{ $options.i18n.createDashboard }} - </gl-new-dropdown-item> + </gl-dropdown-item> <template v-if="isMenuItemEnabled.createDashboard"> <create-dashboard-modal @@ -287,5 +287,5 @@ export default { :project-path="projectPath" /> </template> - </gl-new-dropdown> + </gl-dropdown> </template> diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue index 6a7bf81c643..e468728a954 100644 --- a/app/assets/javascripts/monitoring/components/dashboard_header.vue +++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue @@ -3,17 +3,17 @@ import { debounce } from 'lodash'; import { mapActions, mapState, mapGetters } from 'vuex'; import { GlButton, - GlNewDropdown, + GlDropdown, GlLoadingIcon, - GlNewDropdownItem, - GlNewDropdownHeader, + GlDropdownItem, + GlDropdownSectionHeader, GlSearchBoxByType, GlModalDirective, GlTooltipDirective, + GlIcon, } from '@gitlab/ui'; import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility'; import invalidUrl from '~/lib/utils/invalid_url'; -import Icon from '~/vue_shared/components/icon.vue'; import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; import DashboardsDropdown from './dashboards_dropdown.vue'; @@ -26,12 +26,12 @@ import { timezones } from '../format_date'; export default { components: { - Icon, + GlIcon, GlButton, - GlNewDropdown, + GlDropdown, GlLoadingIcon, - GlNewDropdownItem, - GlNewDropdownHeader, + GlDropdownItem, + GlDropdownSectionHeader, GlSearchBoxByType, @@ -181,7 +181,7 @@ export default { <span aria-hidden="true" class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"></span> <div class="mb-2 pr-2 d-flex d-sm-block"> - <gl-new-dropdown + <gl-dropdown id="monitor-environments-dropdown" ref="monitorEnvironmentsDropdown" class="flex-grow-1" @@ -191,12 +191,12 @@ export default { :text="environmentDropdownText" > <div class="d-flex flex-column overflow-hidden"> - <gl-new-dropdown-header>{{ __('Environment') }}</gl-new-dropdown-header> - <gl-search-box-by-type class="m-2" @input="debouncedEnvironmentsSearch" /> + <gl-dropdown-section-header>{{ __('Environment') }}</gl-dropdown-section-header> + <gl-search-box-by-type class="gl-m-3" @input="debouncedEnvironmentsSearch" /> <gl-loading-icon v-if="environmentsLoading" :inline="true" /> <div v-else class="flex-fill overflow-auto"> - <gl-new-dropdown-item + <gl-dropdown-item v-for="environment in filteredEnvironments" :key="environment.id" :is-check-item="true" @@ -204,7 +204,7 @@ export default { :href="getEnvironmentPath(environment.id)" > {{ environment.name }} - </gl-new-dropdown-item> + </gl-dropdown-item> </div> <div v-show="shouldShowEnvironmentsDropdownNoMatchedMsg" @@ -214,7 +214,7 @@ export default { {{ __('No matching results') }} </div> </div> - </gl-new-dropdown> + </gl-dropdown> </div> <div class="mb-2 pr-2 d-flex d-sm-block"> @@ -260,7 +260,7 @@ export default { target="_blank" rel="noopener noreferrer" > - {{ __('View full dashboard') }} <icon name="external-link" /> + {{ __('View full dashboard') }} <gl-icon name="external-link" /> </gl-button> </div> diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue index 278858d3a94..18310f7c71e 100644 --- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue +++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue @@ -6,9 +6,9 @@ import { GlIcon, GlLink, GlLoadingIcon, - GlNewDropdown as GlDropdown, - GlNewDropdownItem as GlDropdownItem, - GlNewDropdownDivider as GlDropdownDivider, + GlDropdown, + GlDropdownItem, + GlDropdownDivider, GlModal, GlModalDirective, GlSprintf, diff --git a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue index aed27b5ea51..932efeaaf0e 100644 --- a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue +++ b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue @@ -2,10 +2,10 @@ import { mapState, mapGetters } from 'vuex'; import { GlIcon, - GlNewDropdown, - GlNewDropdownItem, - GlNewDropdownHeader, - GlNewDropdownDivider, + GlDropdown, + GlDropdownItem, + GlDropdownSectionHeader, + GlDropdownDivider, GlSearchBoxByType, GlModalDirective, } from '@gitlab/ui'; @@ -17,10 +17,10 @@ const events = { export default { components: { GlIcon, - GlNewDropdown, - GlNewDropdownItem, - GlNewDropdownHeader, - GlNewDropdownDivider, + GlDropdown, + GlDropdownItem, + GlDropdownSectionHeader, + GlDropdownDivider, GlSearchBoxByType, }, directives: { @@ -73,21 +73,21 @@ export default { }; </script> <template> - <gl-new-dropdown + <gl-dropdown toggle-class="dropdown-menu-toggle" menu-class="monitor-dashboard-dropdown-menu" :text="selectedDashboardText" > <div class="d-flex flex-column overflow-hidden"> - <gl-new-dropdown-header>{{ __('Dashboard') }}</gl-new-dropdown-header> + <gl-dropdown-section-header>{{ __('Dashboard') }}</gl-dropdown-section-header> <gl-search-box-by-type ref="monitorDashboardsDropdownSearch" v-model="searchTerm" - class="m-2" + class="gl-m-3" /> <div class="flex-fill overflow-auto"> - <gl-new-dropdown-item + <gl-dropdown-item v-for="dashboard in starredDashboards" :key="dashboard.path" :is-check-item="true" @@ -95,28 +95,28 @@ export default { @click="selectDashboard(dashboard)" > <div class="gl-display-flex"> - <div class="gl-flex-grow-1 gl-min-w-0"> - <div class="gl-word-break-all"> - {{ dashboardDisplayName(dashboard) }} - </div> - </div> - <gl-icon class="text-muted gl-flex-shrink-0" name="star" /> + <span class="gl-flex-grow-1 gl-min-w-0 gl-overflow-hidden gl-overflow-wrap-break"> + {{ dashboardDisplayName(dashboard) }} + </span> + <gl-icon class="text-muted gl-flex-shrink-0 gl-ml-3 gl-align-self-center" name="star" /> </div> - </gl-new-dropdown-item> - <gl-new-dropdown-divider + </gl-dropdown-item> + <gl-dropdown-divider v-if="starredDashboards.length && nonStarredDashboards.length" ref="starredListDivider" /> - <gl-new-dropdown-item + <gl-dropdown-item v-for="dashboard in nonStarredDashboards" :key="dashboard.path" :is-check-item="true" :is-checked="dashboard.path === selectedDashboardPath" @click="selectDashboard(dashboard)" > - {{ dashboardDisplayName(dashboard) }} - </gl-new-dropdown-item> + <span class="gl-overflow-hidden gl-overflow-wrap-break"> + {{ dashboardDisplayName(dashboard) }} + </span> + </gl-dropdown-item> </div> <div @@ -127,5 +127,5 @@ export default { {{ __('No matching results') }} </div> </div> - </gl-new-dropdown> + </gl-dropdown> </template> diff --git a/app/assets/javascripts/monitoring/components/embeds/embed_group.vue b/app/assets/javascripts/monitoring/components/embeds/embed_group.vue index b60c87fee82..f07483c34b8 100644 --- a/app/assets/javascripts/monitoring/components/embeds/embed_group.vue +++ b/app/assets/javascripts/monitoring/components/embeds/embed_group.vue @@ -1,14 +1,14 @@ <script> import { mapState, mapActions, mapGetters } from 'vuex'; import sum from 'lodash/sum'; -import { GlDeprecatedButton, GlCard, GlIcon } from '@gitlab/ui'; +import { GlButton, GlCard, GlIcon } from '@gitlab/ui'; import { n__ } from '~/locale'; import { monitoringDashboard } from '~/monitoring/stores'; import MetricEmbed from './metric_embed.vue'; export default { components: { - GlDeprecatedButton, + GlButton, GlCard, GlIcon, MetricEmbed, @@ -78,15 +78,16 @@ export default { :body-class="bodyClass" > <template #header> - <gl-deprecated-button - class="collapsible-card-btn d-flex text-decoration-none" + <gl-button + class="collapsible-card-btn gl-display-flex gl-text-decoration-none gl-reset-color! gl-hover-text-blue-800! gl-shadow-none!" :aria-label="buttonLabel" variant="link" + category="tertiary" @click="toggleCollapsed" > <gl-icon class="mr-1" :name="arrowIconName" /> {{ buttonLabel }} - </gl-deprecated-button> + </gl-button> </template> <div class="d-flex flex-wrap"> <metric-embed diff --git a/app/assets/javascripts/monitoring/components/group_empty_state.vue b/app/assets/javascripts/monitoring/components/group_empty_state.vue index 9cf492dd537..499823fae3f 100644 --- a/app/assets/javascripts/monitoring/components/group_empty_state.vue +++ b/app/assets/javascripts/monitoring/components/group_empty_state.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { GlEmptyState } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import { metricStates } from '../constants'; diff --git a/app/assets/javascripts/monitoring/components/refresh_button.vue b/app/assets/javascripts/monitoring/components/refresh_button.vue index 0e9605450ed..e0d9f92440b 100644 --- a/app/assets/javascripts/monitoring/components/refresh_button.vue +++ b/app/assets/javascripts/monitoring/components/refresh_button.vue @@ -4,9 +4,9 @@ import { mapActions } from 'vuex'; import { GlButtonGroup, GlButton, - GlNewDropdown, - GlNewDropdownItem, - GlNewDropdownDivider, + GlDropdown, + GlDropdownItem, + GlDropdownDivider, GlTooltipDirective, } from '@gitlab/ui'; import { n__, __ } from '~/locale'; @@ -48,9 +48,9 @@ export default { components: { GlButtonGroup, GlButton, - GlNewDropdown, - GlNewDropdownItem, - GlNewDropdownDivider, + GlDropdown, + GlDropdownItem, + GlDropdownDivider, }, directives: { GlTooltip: GlTooltipDirective, @@ -152,27 +152,27 @@ export default { icon="retry" @click="refresh" /> - <gl-new-dropdown + <gl-dropdown v-if="!disableMetricDashboardRefreshRate" v-gl-tooltip :title="s__('Metrics|Set refresh rate')" :text="dropdownText" > - <gl-new-dropdown-item + <gl-dropdown-item :is-check-item="true" :is-checked="refreshInterval === null" @click="removeRefreshInterval()" - >{{ __('Off') }}</gl-new-dropdown-item + >{{ __('Off') }}</gl-dropdown-item > - <gl-new-dropdown-divider /> - <gl-new-dropdown-item + <gl-dropdown-divider /> + <gl-dropdown-item v-for="(option, i) in $options.refreshIntervals" :key="i" :is-check-item="true" :is-checked="isChecked(option)" @click="setRefreshInterval(option)" - >{{ option.label }}</gl-new-dropdown-item + >{{ option.label }}</gl-dropdown-item > - </gl-new-dropdown> + </gl-dropdown> </gl-button-group> </template> diff --git a/app/assets/javascripts/monitoring/components/variables_section.vue b/app/assets/javascripts/monitoring/components/variables_section.vue index 25d900b07ad..7c4fb135ec8 100644 --- a/app/assets/javascripts/monitoring/components/variables_section.vue +++ b/app/assets/javascripts/monitoring/components/variables_section.vue @@ -37,7 +37,11 @@ export default { }; </script> <template> - <div ref="variablesSection" class="d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 variables-section"> + <div + ref="variablesSection" + class="d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 variables-section" + data-qa-selector="variables_content" + > <div v-for="variable in variables" :key="variable.name" class="mb-1 pr-2 d-flex d-sm-block"> <component :is="variableField(variable.type)" @@ -46,6 +50,7 @@ export default { :value="variable.value" :name="variable.name" :options="variable.options" + data-qa-selector="variable_item" @input="refreshDashboard(variable, $event)" /> </div> diff --git a/app/assets/javascripts/monitoring/csv_export.js b/app/assets/javascripts/monitoring/csv_export.js index 734e8dc07a7..20cfa23e9b4 100644 --- a/app/assets/javascripts/monitoring/csv_export.js +++ b/app/assets/javascripts/monitoring/csv_export.js @@ -125,7 +125,6 @@ const csvData = (metricHeaders, metricValues) => { * @param {Object} graphData - Panel contents * @returns {String} */ -// eslint-disable-next-line import/prefer-default-export export const graphDataToCsv = graphData => { const delimiter = ','; const br = '\r\n'; diff --git a/app/assets/javascripts/monitoring/stores/embed_group/actions.js b/app/assets/javascripts/monitoring/stores/embed_group/actions.js index 4a7572bdbd9..ca0d2e5ba35 100644 --- a/app/assets/javascripts/monitoring/stores/embed_group/actions.js +++ b/app/assets/javascripts/monitoring/stores/embed_group/actions.js @@ -1,4 +1,3 @@ import * as types from './mutation_types'; -// eslint-disable-next-line import/prefer-default-export export const addModule = ({ commit }, data) => commit(types.ADD_MODULE, data); diff --git a/app/assets/javascripts/monitoring/stores/embed_group/getters.js b/app/assets/javascripts/monitoring/stores/embed_group/getters.js index 096d8d03096..47db787dea5 100644 --- a/app/assets/javascripts/monitoring/stores/embed_group/getters.js +++ b/app/assets/javascripts/monitoring/stores/embed_group/getters.js @@ -1,3 +1,2 @@ -// eslint-disable-next-line import/prefer-default-export export const metricsWithData = (state, getters, rootState, rootGetters) => state.modules.map(module => rootGetters[`${module}/metricsWithData`]().length); diff --git a/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js b/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js index 7fd3f0f8647..288e6db4151 100644 --- a/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js +++ b/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js @@ -1,2 +1 @@ -// eslint-disable-next-line import/prefer-default-export export const ADD_MODULE = 'ADD_MODULE'; diff --git a/app/assets/javascripts/mr_popover/components/mr_popover.vue b/app/assets/javascripts/mr_popover/components/mr_popover.vue index e6bf7a6ec02..bf810978648 100644 --- a/app/assets/javascripts/mr_popover/components/mr_popover.vue +++ b/app/assets/javascripts/mr_popover/components/mr_popover.vue @@ -1,6 +1,6 @@ <script> /* eslint-disable @gitlab/vue-require-i18n-strings */ -import { GlPopover, GlSkeletonLoading } from '@gitlab/ui'; +import { GlPopover, GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import CiIcon from '../../vue_shared/components/ci_icon.vue'; import timeagoMixin from '../../vue_shared/mixins/timeago'; import query from '../queries/merge_request.query.graphql'; diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js index b96a111cf13..8e123c14814 100644 --- a/app/assets/javascripts/namespace_select.js +++ b/app/assets/javascripts/namespace_select.js @@ -1,16 +1,16 @@ import $ from 'jquery'; -import '~/gl_dropdown'; import Api from './api'; import { mergeUrlParams } from './lib/utils/url_utility'; import { parseBoolean } from '~/lib/utils/common_utils'; import { __ } from './locale'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default class NamespaceSelect { constructor(opts) { const isFilter = parseBoolean(opts.dropdown.dataset.isFilter); const fieldName = opts.dropdown.dataset.fieldName || 'namespace_id'; - $(opts.dropdown).glDropdown({ + initDeprecatedJQueryDropdown($(opts.dropdown), { filterable: true, selectable: true, filterRemote: true, diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue index fa1afdcd16f..3bbaa44ec42 100644 --- a/app/assets/javascripts/notebook/cells/markdown.vue +++ b/app/assets/javascripts/notebook/cells/markdown.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import marked from 'marked'; import { sanitize } from 'dompurify'; import katex from 'katex'; diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue index b36761993ea..856c8f31796 100644 --- a/app/assets/javascripts/notebook/cells/output/html.vue +++ b/app/assets/javascripts/notebook/cells/output/html.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { sanitize } from 'dompurify'; import Prompt from '../prompt.vue'; diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 3940b4b4724..340fbe4d887 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -11,13 +11,12 @@ old_notes_spec.js is the spec for the legacy, jQuery notes application. It has n */ import $ from 'jquery'; +import '~/lib/utils/jquery_at_who'; import { escape, uniqueId } from 'lodash'; import Cookies from 'js-cookie'; import Autosize from 'autosize'; -import 'jquery.caret'; // required by at.js -import '@gitlab/at.js'; import Vue from 'vue'; -import { GlSkeletonLoading } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import AjaxCache from '~/lib/utils/ajax_cache'; import syntaxHighlight from '~/syntax_highlight'; import axios from './lib/utils/axios_utils'; diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 7cfff98e9f7..54fcf41ca50 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -3,7 +3,7 @@ import $ from 'jquery'; import { mapActions, mapGetters, mapState } from 'vuex'; import { isEmpty } from 'lodash'; import Autosize from 'autosize'; -import { GlAlert, GlIntersperse, GlLink, GlSprintf } from '@gitlab/ui'; +import { GlAlert, GlIntersperse, GlLink, GlSprintf, GlButton } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import { deprecatedCreateFlash as Flash } from '../../flash'; @@ -20,7 +20,6 @@ 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 loadingButton from '../../vue_shared/components/loading_button.vue'; import noteSignedOutWidget from './note_signed_out_widget.vue'; import discussionLockedWidget from './discussion_locked_widget.vue'; import issuableStateMixin from '../mixins/issuable_state'; @@ -33,7 +32,7 @@ export default { discussionLockedWidget, markdownField, userAvatarLink, - loadingButton, + GlButton, TimelineEntryItem, GlAlert, GlIntersperse, @@ -102,6 +101,9 @@ export default { noteable: this.noteableDisplayName, }); }, + buttonVariant() { + return this.isOpen ? 'warning' : 'default'; + }, actionButtonClassNames() { return { 'btn-reopen': !this.isOpen, @@ -378,7 +380,7 @@ export default { dir="auto" :disabled="isSubmitting" name="note[note]" - class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input" + class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area qa-comment-input" data-supports-quick-actions="true" :aria-label="__('Description')" :placeholder="__('Write a comment or drag your files here…')" @@ -395,7 +397,7 @@ export default { :secondary-button-text="__('Cancel')" variant="warning" :dismissible="false" - @primaryAction="forceCloseIssue" + @primaryAction="toggleBlockedIssueWarning(false) && forceCloseIssue()" @secondaryAction="toggleBlockedIssueWarning(false) && enableButton()" > <p> @@ -421,27 +423,28 @@ export default { <div class="btn-group gl-mr-3 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" > - <button + <gl-button :disabled="isSubmitButtonDisabled" - class="btn btn-success js-comment-button js-comment-submit-button qa-comment-button" + class="js-comment-button js-comment-submit-button qa-comment-button" type="submit" + category="primary" + variant="success" :data-track-label="trackingLabel" data-track-event="click_button" @click.prevent="handleSave()" + >{{ commentButtonTitle }}</gl-button > - {{ commentButtonTitle }} - </button> - <button + <gl-button :disabled="isSubmitButtonDisabled" name="button" - type="button" - class="btn btn-success note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown" + category="primary" + variant="success" + class="note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown" data-display="static" data-toggle="dropdown" + icon="chevron-down" :aria-label="__('Open comment type dropdown')" - > - <i aria-hidden="true" class="fa fa-caret-down toggle-icon"></i> - </button> + /> <ul class="note-type-dropdown dropdown-open-top dropdown-menu"> <li :class="{ 'droplab-item-selected': noteType === 'comment' }"> @@ -465,11 +468,7 @@ export default { </li> <li class="divider droplab-item-ignore"></li> <li :class="{ 'droplab-item-selected': noteType === 'discussion' }"> - <button - type="button" - class="btn btn-transparent qa-discussion-option" - @click.prevent="setNoteType('discussion')" - > + <button class="qa-discussion-option" @click.prevent="setNoteType('discussion')"> <i aria-hidden="true" class="fa fa-check icon"></i> <div class="description"> <strong>{{ __('Start thread') }}</strong> @@ -480,17 +479,19 @@ export default { </ul> </div> - <loading-button + <gl-button v-if="canToggleIssueState && !isToggleBlockedIssueWarning" :loading="isToggleStateButtonLoading" - :container-class="[ + category="secondary" + :variant="buttonVariant" + :class="[ actionButtonClassNames, - 'btn btn-comment btn-comment-and-close js-action-button', + 'btn-comment btn-comment-and-close js-action-button', ]" :disabled="isToggleStateButtonLoading || isSubmitting" - :label="issueActionButtonTitle" @click="handleSave(true)" - /> + >{{ issueActionButtonTitle }}</gl-button + > </div> </form> </div> diff --git a/app/assets/javascripts/notes/components/diff_discussion_header.vue b/app/assets/javascripts/notes/components/diff_discussion_header.vue index 50d224a2f08..8e6c01ba63f 100644 --- a/app/assets/javascripts/notes/components/diff_discussion_header.vue +++ b/app/assets/javascripts/notes/components/diff_discussion_header.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { mapActions } from 'vuex'; import { escape } from 'lodash'; diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index 8897b54fac7..c01cd8f8037 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -1,6 +1,7 @@ <script> +/* eslint-disable vue/no-v-html */ import { mapState, mapActions } from 'vuex'; -import { GlSkeletonLoading } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue'; diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index 4a1a1086329..c6fab271376 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -1,7 +1,6 @@ <script> import { mapGetters, mapActions } from 'vuex'; -import { GlTooltipDirective } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; import discussionNavigation from '../mixins/discussion_navigation'; export default { @@ -9,7 +8,7 @@ export default { GlTooltip: GlTooltipDirective, }, components: { - Icon, + GlIcon, }, mixins: [discussionNavigation], computed: { @@ -60,7 +59,7 @@ export default { :class="{ 'line-resolve-btn is-active': allResolved, 'line-resolve-text': !allResolved }" > <template v-if="allResolved"> - <icon name="check-circle-filled" /> + <gl-icon name="check-circle-filled" /> {{ __('All threads resolved') }} </template> <template v-else> @@ -79,7 +78,7 @@ export default { :title="s__('Resolve all threads in new issue')" class="new-issue-for-discussion btn btn-default discussion-create-issue-btn" > - <icon name="issue-new" /> + <gl-icon name="issue-new" /> </a> </div> <div v-if="isLoggedIn && !allResolved" class="btn-group btn-group-sm" role="group"> @@ -92,7 +91,7 @@ export default { data-track-property="click_next_unresolved_thread_top" @click="jumpToNextDiscussion" > - <icon name="comment-next" /> + <gl-icon name="comment-next" /> </button> </div> <div class="btn-group btn-group-sm" role="group"> @@ -102,7 +101,7 @@ export default { class="btn btn-default toggle-all-discussions-btn" @click="handleExpandDiscussions" > - <icon :name="allExpanded ? 'angle-up' : 'angle-down'" /> + <gl-icon :name="allExpanded ? 'angle-up' : 'angle-down'" /> </button> </div> </div> diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue index 6b1e3298f9a..989ce9ff144 100644 --- a/app/assets/javascripts/notes/components/discussion_filter.vue +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -1,8 +1,8 @@ <script> import $ from 'jquery'; import { mapGetters, mapActions } from 'vuex'; +import { GlIcon } from '@gitlab/ui'; import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility'; -import Icon from '~/vue_shared/components/icon.vue'; import { DISCUSSION_FILTERS_DEFAULT_VALUE, HISTORY_ONLY_FILTER_VALUE, @@ -14,7 +14,7 @@ import notesEventHub from '../event_hub'; export default { components: { - Icon, + GlIcon, }, props: { filters: { @@ -120,7 +120,7 @@ export default { data-toggle="dropdown" aria-expanded="false" > - {{ currentFilter.title }} <icon name="chevron-down" /> + {{ currentFilter.title }} <gl-icon name="chevron-down" /> </button> <div ref="dropdownMenu" diff --git a/app/assets/javascripts/notes/components/discussion_filter_note.vue b/app/assets/javascripts/notes/components/discussion_filter_note.vue index 8dc4b43d69a..ae6646cf96c 100644 --- a/app/assets/javascripts/notes/components/discussion_filter_note.vue +++ b/app/assets/javascripts/notes/components/discussion_filter_note.vue @@ -1,6 +1,6 @@ <script> -import { GlButton } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +/* eslint-disable vue/no-v-html */ +import { GlButton, GlIcon } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import notesEventHub from '../event_hub'; @@ -8,7 +8,7 @@ import notesEventHub from '../event_hub'; export default { components: { GlButton, - Icon, + GlIcon, }, computed: { timelineContent() { @@ -35,7 +35,7 @@ export default { <template> <li class="timeline-entry note note-wrapper discussion-filter-note js-discussion-filter-note"> <div class="timeline-icon d-none d-lg-flex"> - <icon name="comment" /> + <gl-icon name="comment" /> </div> <div class="timeline-content"> <div ref="timelineContent" v-html="timelineContent"></div> 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 index b71ce1b6a0a..f94d0060b41 100644 --- a/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue +++ b/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue @@ -1,12 +1,11 @@ <script> -import { GlTooltipDirective } from '@gitlab/ui'; -import icon from '~/vue_shared/components/icon.vue'; +import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; import discussionNavigation from '../mixins/discussion_navigation'; export default { name: 'JumpToNextDiscussionButton', components: { - icon, + GlIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -33,7 +32,7 @@ export default { data-track-property="click_next_unresolved_thread" @click="jumpToNextRelativeDiscussion(fromDiscussionId)" > - <icon name="comment-next" /> + <gl-icon name="comment-next" /> </button> </div> </template> diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue index 8636984c6af..2f215e36d5b 100644 --- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue +++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue @@ -1,13 +1,12 @@ <script> -import { GlLink } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlLink, GlIcon } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import Issuable from '~/vue_shared/mixins/issuable'; import issuableStateMixin from '../mixins/issuable_state'; export default { components: { - Icon, + GlIcon, GlLink, }, mixins: [Issuable, issuableStateMixin], @@ -28,7 +27,7 @@ export default { <template> <div class="disabled-comment text-center"> <span class="issuable-note-warning inline"> - <icon :size="16" name="lock" class="icon" /> + <gl-icon :size="16" name="lock" class="icon" /> <span v-if="isProjectArchived"> {{ projectArchivedWarning }} <gl-link :href="archivedProjectDocsPath" target="_blank" class="learn-more"> diff --git a/app/assets/javascripts/notes/components/discussion_resolve_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_button.vue index 77f6f1e51c5..e060a6affd4 100644 --- a/app/assets/javascripts/notes/components/discussion_resolve_button.vue +++ b/app/assets/javascripts/notes/components/discussion_resolve_button.vue @@ -1,10 +1,10 @@ <script> -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlButton } from '@gitlab/ui'; export default { name: 'ResolveDiscussionButton', components: { - GlLoadingIcon, + GlButton, }, props: { isResolving: { @@ -21,8 +21,7 @@ export default { </script> <template> - <button ref="button" type="button" class="btn btn-default ml-sm-2" @click="$emit('onClick')"> - <gl-loading-icon v-if="isResolving" ref="isResolvingIcon" inline /> + <gl-button :loading="isResolving" class="ml-sm-2" @click="$emit('onClick')"> {{ buttonTitle }} - </button> + </gl-button> </template> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index a8ae7fb48f0..a8057276f1a 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -1,18 +1,18 @@ <script> import { mapGetters } from 'vuex'; -import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; -import { __ } from '~/locale'; +import { GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui'; +import { __, sprintf } from '~/locale'; import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status'; -import Icon from '~/vue_shared/components/icon.vue'; import ReplyButton from './note_actions/reply_button.vue'; import eventHub from '~/sidebar/event_hub'; import Api from '~/api'; import { deprecatedCreateFlash as flash } from '~/flash'; +import { splitCamelCase } from '../../lib/utils/text_utility'; export default { name: 'NoteActions', components: { - Icon, + GlIcon, ReplyButton, GlLoadingIcon, }, @@ -48,6 +48,26 @@ export default { required: false, default: null, }, + isAuthor: { + type: Boolean, + required: false, + default: false, + }, + isContributor: { + type: Boolean, + required: false, + default: false, + }, + noteableType: { + type: String, + required: false, + default: '', + }, + projectName: { + type: String, + required: false, + default: '', + }, showReply: { type: Boolean, required: true, @@ -122,6 +142,9 @@ export default { targetType() { return this.getNoteableData.targetType; }, + noteableDisplayName() { + return splitCamelCase(this.noteableType).toLowerCase(); + }, assignees() { return this.getNoteableData.assignees || []; }, @@ -131,6 +154,22 @@ export default { canAssign() { return this.getNoteableData.current_user?.can_update && this.isIssue; }, + displayAuthorBadgeText() { + return sprintf(__('This user is the author of this %{noteable}.'), { + noteable: this.noteableDisplayName, + }); + }, + displayMemberBadgeText() { + return sprintf(__('This user is a %{access} of the %{name} project.'), { + access: this.accessLevel.toLowerCase(), + name: this.projectName, + }); + }, + displayContributorBadgeText() { + return sprintf(__('This user has previously committed to the %{name} project.'), { + name: this.projectName, + }); + }, }, methods: { onEdit() { @@ -176,7 +215,24 @@ export default { <template> <div class="note-actions"> - <span v-if="accessLevel" class="note-role user-access-role">{{ accessLevel }}</span> + <span + v-if="isAuthor" + class="note-role user-access-role has-tooltip d-none d-md-inline-block" + :title="displayAuthorBadgeText" + >{{ __('Author') }}</span + > + <span + v-if="accessLevel" + class="note-role user-access-role has-tooltip" + :title="displayMemberBadgeText" + >{{ accessLevel }}</span + > + <span + v-else-if="isContributor" + class="note-role user-access-role has-tooltip" + :title="displayContributorBadgeText" + >{{ __('Contributor') }}</span + > <div v-if="canResolve" class="note-actions-item"> <button ref="resolveButton" @@ -189,7 +245,7 @@ export default { @click="onResolve" > <template v-if="!isResolving"> - <icon :name="isResolved ? 'check-circle-filled' : 'check-circle'" /> + <gl-icon :name="isResolved ? 'check-circle-filled' : 'check-circle'" /> </template> <gl-loading-icon v-else inline /> </button> @@ -203,9 +259,9 @@ export default { title="Add reaction" data-position="right" > - <icon class="link-highlight award-control-icon-neutral" name="slight-smile" /> - <icon class="link-highlight award-control-icon-positive" name="smiley" /> - <icon class="link-highlight award-control-icon-super-positive" name="smiley" /> + <gl-icon class="link-highlight award-control-icon-neutral" name="slight-smile" /> + <gl-icon class="link-highlight award-control-icon-positive" name="smiley" /> + <gl-icon class="link-highlight award-control-icon-super-positive" name="smiley" /> </a> </div> <reply-button @@ -222,7 +278,7 @@ export default { class="note-action-button js-note-edit btn btn-transparent qa-note-edit-button" @click="onEdit" > - <icon name="pencil" class="link-highlight" /> + <gl-icon name="pencil" class="link-highlight" /> </button> </div> <div v-if="showDeleteAction" class="note-actions-item"> @@ -233,7 +289,7 @@ export default { class="note-action-button js-note-delete btn btn-transparent" @click="onDelete" > - <icon name="remove" class="link-highlight" /> + <gl-icon name="remove" class="link-highlight" /> </button> </div> <div v-else-if="shouldShowActionsDropdown" class="dropdown more-actions note-actions-item"> @@ -245,7 +301,7 @@ export default { data-toggle="dropdown" @click="closeTooltip" > - <icon class="icon" name="ellipsis_v" /> + <gl-icon class="icon" name="ellipsis_v" /> </button> <ul class="dropdown-menu more-actions-dropdown dropdown-open-left"> <li v-if="canReportAsAbuse"> diff --git a/app/assets/javascripts/notes/components/note_actions/reply_button.vue b/app/assets/javascripts/notes/components/note_actions/reply_button.vue index 30cb7967c34..f19b7667fb2 100644 --- a/app/assets/javascripts/notes/components/note_actions/reply_button.vue +++ b/app/assets/javascripts/notes/components/note_actions/reply_button.vue @@ -1,12 +1,10 @@ <script> -import { GlTooltipDirective, GlDeprecatedButton } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlTooltipDirective, GlButton } from '@gitlab/ui'; export default { name: 'ReplyButton', components: { - Icon, - GlDeprecatedButton, + GlButton, }, directives: { GlTooltip: GlTooltipDirective, @@ -16,17 +14,17 @@ export default { <template> <div class="note-actions-item"> - <gl-deprecated-button + <gl-button ref="button" v-gl-tooltip - class="note-action-button" data-track-event="click_button" data-track-label="reply_comment_button" - variant="transparent" + category="tertiary" + size="small" + icon="comment" :title="__('Reply to comment')" + :aria-label="__('Reply to comment')" @click="$emit('startReplying')" - > - <icon name="comment" class="link-highlight" /> - </gl-deprecated-button> + /> </div> </template> diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index 42b78929f8a..314fa762768 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { mapActions, mapGetters, mapState } from 'vuex'; import $ from 'jquery'; import '~/behaviors/markdown/render_gfm'; diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 24227d55ebf..88b4461cf38 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { mapGetters, mapActions, mapState } from 'vuex'; import { mergeUrlParams } from '~/lib/utils/url_utility'; import eventHub from '../event_hub'; @@ -336,7 +337,7 @@ export default { 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 js-vue-textarea qa-reply-input" + class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form qa-reply-input" dir="auto" :aria-label="__('Description')" :placeholder="__('Write a comment or drag your files here…')" diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index 9ded5ab648e..a13a0dbbf30 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -1,6 +1,7 @@ <script> +/* eslint-disable vue/no-v-html */ import { mapActions } from 'vuex'; -import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import { GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; export default { @@ -9,6 +10,7 @@ export default { GitlabTeamMemberBadge: () => import('ee_component/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue'), GlIcon, + GlLoadingIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -194,13 +196,12 @@ export default { class="gl-ml-1 gl-text-gray-700 align-middle" /> <slot name="extra-controls"></slot> - <i + <gl-loading-icon v-if="showSpinner" ref="spinner" - class="fa fa-spinner fa-spin editing-spinner" - :aria-label="__('Comment is being updated')" - aria-hidden="true" - ></i> + class="editing-spinner" + :label="__('Comment is being updated')" + /> </span> </div> </template> diff --git a/app/assets/javascripts/notes/components/note_signed_out_widget.vue b/app/assets/javascripts/notes/components/note_signed_out_widget.vue index ccfe84ab098..593933016e1 100644 --- a/app/assets/javascripts/notes/components/note_signed_out_widget.vue +++ b/app/assets/javascripts/notes/components/note_signed_out_widget.vue @@ -1,8 +1,12 @@ <script> +import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import { mapGetters } from 'vuex'; import { __, sprintf } from '~/locale'; export default { + directives: { + SafeHtml, + }, computed: { ...mapGetters(['getNotesDataByProp']), registerLink() { @@ -30,5 +34,5 @@ export default { </script> <template> - <div class="disabled-comment text-center" v-html="signedOutText"></div> + <div v-safe-html="signedOutText" class="disabled-comment text-center"></div> </template> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index b4176c6063b..62ee7f30c57 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -1,10 +1,9 @@ <script> import { mapActions, mapGetters } from 'vuex'; -import { GlTooltipDirective } from '@gitlab/ui'; +import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form'; import { s__, __ } from '~/locale'; import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave'; -import icon from '~/vue_shared/components/icon.vue'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import DraftNote from '~/batch_comments/components/draft_note.vue'; import { deprecatedCreateFlash as Flash } from '../../flash'; @@ -22,7 +21,7 @@ import DiscussionActions from './discussion_actions.vue'; export default { name: 'NoteableDiscussion', components: { - icon, + GlIcon, userAvatarLink, diffDiscussionHeader, noteSignedOutWidget, diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index ce771e67cbb..4f45fcb0062 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -2,7 +2,7 @@ import $ from 'jquery'; import { mapGetters, mapActions } from 'vuex'; import { escape } from 'lodash'; -import { GlSprintf } from '@gitlab/ui'; +import { GlSprintf, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { truncateSha } from '~/lib/utils/text_utility'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; @@ -34,6 +34,9 @@ export default { NoteBody, TimelineEntryItem, }, + directives: { + SafeHtml, + }, mixins: [noteable, resolvable, glFeatureFlagsMixin()], props: { note: { @@ -358,7 +361,7 @@ export default { </template> </gl-sprintf> </div> - <div v-once class="timeline-icon"> + <div class="timeline-icon"> <user-avatar-link :link-href="author.path" :img-src="author.avatar_url" @@ -371,14 +374,13 @@ export default { <div class="timeline-content"> <div class="note-header"> <note-header - v-once :author="author" :created-at="note.created_at" :note-id="note.id" :is-confidential="note.confidential" > <slot slot="note-header-info" name="note-header-info"></slot> - <span v-if="commit" v-html="actionText"></span> + <span v-if="commit" v-safe-html="actionText"></span> <span v-else-if="note.created_at" class="d-none d-sm-inline">·</span> </note-header> <note-actions @@ -387,6 +389,10 @@ export default { :note-id="note.id" :note-url="note.noteable_note_url" :access-level="note.human_access" + :is-contributor="note.is_contributor" + :is-author="note.is_noteable_author" + :project-name="note.project_name" + :noteable-type="note.noteable_type" :show-reply="showReplyButton" :can-edit="note.current_user.can_edit" :can-award-emoji="note.current_user.can_award_emoji" diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue index dd132d4f608..bddac60647d 100644 --- a/app/assets/javascripts/notes/components/toggle_replies_widget.vue +++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue @@ -1,12 +1,12 @@ <script> import { uniqBy } from 'lodash'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; export default { components: { - Icon, + GlIcon, UserAvatarLink, TimeAgoTooltip, }, @@ -44,7 +44,7 @@ export default { <template> <li :class="className" class="replies-toggle js-toggle-replies"> <template v-if="collapsed"> - <icon name="chevron-right" @click.native="toggle" /> + <gl-icon name="chevron-right" @click.native="toggle" /> <div> <user-avatar-link v-for="author in uniqueAuthors" @@ -71,7 +71,7 @@ export default { class="collapse-replies-btn js-collapse-replies qa-collapse-replies" @click="toggle" > - <icon name="chevron-down" /> {{ s__('Notes|Collapse replies') }} + <gl-icon name="chevron-down" /> {{ s__('Notes|Collapse replies') }} </span> </li> </template> diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js index c1449237f8a..b81aae7c257 100644 --- a/app/assets/javascripts/notes/constants.js +++ b/app/assets/javascripts/notes/constants.js @@ -22,6 +22,8 @@ export const TIME_DIFFERENCE_VALUE = 10; export const ASC = 'asc'; export const DESC = 'desc'; +export const DISCUSSION_FETCH_TIMEOUT = 750; + export const NOTEABLE_TYPE_MAPPING = { Issue: ISSUE_NOTEABLE_TYPE, MergeRequest: MERGE_REQUEST_NOTEABLE_TYPE, diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index f6069b509e8..9c63a7e3cd4 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -87,6 +87,7 @@ export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFi return axios.get(path, config).then(({ data }) => { commit(types.SET_INITIAL_DISCUSSIONS, data); + commit(types.SET_FETCHING_DISCUSSIONS, false); dispatch('updateResolvableDiscussionsCounts'); }); @@ -136,6 +137,23 @@ export const updateNote = ({ commit, dispatch }, { endpoint, note }) => export const updateOrCreateNotes = ({ commit, state, getters, dispatch }, notes) => { const { notesById } = getters; + const debouncedFetchDiscussions = isFetching => { + if (!isFetching) { + commit(types.SET_FETCHING_DISCUSSIONS, true); + dispatch('fetchDiscussions', { path: state.notesData.discussionsPath }); + } else { + if (isFetching !== true) { + clearTimeout(state.currentlyFetchingDiscussions); + } + + commit( + types.SET_FETCHING_DISCUSSIONS, + setTimeout(() => { + dispatch('fetchDiscussions', { path: state.notesData.discussionsPath }); + }, constants.DISCUSSION_FETCH_TIMEOUT), + ); + } + }; notes.forEach(note => { if (notesById[note.id]) { @@ -146,7 +164,7 @@ export const updateOrCreateNotes = ({ commit, state, getters, dispatch }, notes) if (discussion) { commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note); } else if (note.type === constants.DIFF_NOTE) { - dispatch('fetchDiscussions', { path: state.notesData.discussionsPath }); + debouncedFetchDiscussions(state.currentlyFetchingDiscussions); } else { commit(types.ADD_NEW_NOTE, note); } @@ -457,7 +475,7 @@ export const poll = ({ commit, state, getters, dispatch }) => { }); if (!Visibility.hidden()) { - eTagPoll.makeRequest(); + eTagPoll.makeDelayedRequest(2500); } else { dispatch('fetchData'); } diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index 1649e63c61f..161c9b8b1b5 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -12,6 +12,7 @@ export default () => ({ lastFetchedAt: null, currentDiscussionId: null, batchSuggestionsInfo: [], + currentlyFetchingDiscussions: false, /** * selectedCommentPosition & selectedCommentPosition structures are the same as `position.line_range`: * { diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index eb3447291bc..23515cdd9e3 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -36,6 +36,7 @@ export const SET_CURRENT_DISCUSSION_ID = 'SET_CURRENT_DISCUSSION_ID'; export const SET_DISCUSSIONS_SORT = 'SET_DISCUSSIONS_SORT'; export const SET_SELECTED_COMMENT_POSITION = 'SET_SELECTED_COMMENT_POSITION'; export const SET_SELECTED_COMMENT_POSITION_HOVER = 'SET_SELECTED_COMMENT_POSITION_HOVER'; +export const SET_FETCHING_DISCUSSIONS = 'SET_FETCHING_DISCUSSIONS'; // Issue export const CLOSE_ISSUE = 'CLOSE_ISSUE'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index aa078f00569..a8bd94cc763 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -379,4 +379,7 @@ export default { [types.UPDATE_ASSIGNEES](state, assignees) { state.noteableData.assignees = assignees; }, + [types.SET_FETCHING_DISCUSSIONS](state, value) { + state.currentlyFetchingDiscussions = value; + }, }; diff --git a/app/assets/javascripts/packages/details/components/additional_metadata.vue b/app/assets/javascripts/packages/details/components/additional_metadata.vue index a3de6dd46c7..76e0976ac05 100644 --- a/app/assets/javascripts/packages/details/components/additional_metadata.vue +++ b/app/assets/javascripts/packages/details/components/additional_metadata.vue @@ -1,7 +1,7 @@ <script> import { GlLink, GlSprintf } from '@gitlab/ui'; import { s__ } from '~/locale'; -import DetailsRow from '~/registry/shared/components/details_row.vue'; +import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; import { generateConanRecipe } from '../utils'; import { PackageType } from '../../shared/constants'; diff --git a/app/assets/javascripts/packages/details/components/app.vue b/app/assets/javascripts/packages/details/components/app.vue index dbb5f7be0a0..af3220840a6 100644 --- a/app/assets/javascripts/packages/details/components/app.vue +++ b/app/assets/javascripts/packages/details/components/app.vue @@ -25,8 +25,9 @@ import { numberToHumanSize } from '~/lib/utils/number_utils'; import timeagoMixin from '~/vue_shared/mixins/timeago'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import { __, s__ } from '~/locale'; -import { PackageType, TrackingActions } from '../../shared/constants'; +import { PackageType, TrackingActions, SHOW_DELETE_SUCCESS_ALERT } from '../../shared/constants'; import { packageTypeToTrackCategory } from '../../shared/utils'; +import { objectToQueryString } from '~/lib/utils/common_utils'; export default { name: 'PackagesApp', @@ -62,17 +63,15 @@ export default { 'packageFiles', 'isLoading', 'canDelete', - 'destroyPath', 'svgPath', 'npmPath', 'npmHelpPath', + 'projectListUrl', + 'groupListUrl', ]), isValidPackage() { return Boolean(this.packageEntity.name); }, - canDeletePackage() { - return this.canDelete && this.destroyPath; - }, filesTableRows() { return this.packageFiles.map(x => ({ name: x.file_name, @@ -100,7 +99,7 @@ export default { }, }, methods: { - ...mapActions(['fetchPackageVersions']), + ...mapActions(['deletePackage', 'fetchPackageVersions']), formatSize(size) { return numberToHumanSize(size); }, @@ -112,6 +111,16 @@ export default { this.fetchPackageVersions(); } }, + async confirmPackageDeletion() { + this.track(TrackingActions.DELETE_PACKAGE); + await this.deletePackage(); + const returnTo = + !this.groupListUrl || document.referrer.includes(this.projectName) + ? this.projectListUrl + : this.groupListUrl; // to avoid security issue url are supplied from backend + const modalQuery = objectToQueryString({ [SHOW_DELETE_SUCCESS_ALERT]: true }); + window.location.replace(`${returnTo}?${modalQuery}`); + }, }, i18n: { deleteModalTitle: s__(`PackageRegistry|Delete Package Version`), @@ -147,12 +156,10 @@ export default { /> <div v-else class="packages-app"> - <div class="detail-page-header d-flex justify-content-between flex-column flex-sm-row"> - <package-title /> - - <div class="mt-sm-2"> + <package-title> + <template #delete-button> <gl-button - v-if="canDeletePackage" + v-if="canDelete" v-gl-modal="'delete-modal'" class="js-delete-button" variant="danger" @@ -161,8 +168,8 @@ export default { > {{ __('Delete') }} </gl-button> - </div> - </div> + </template> + </package-title> <gl-tabs> <gl-tab :title="__('Detail')"> @@ -268,22 +275,22 @@ export default { </template> </gl-sprintf> - <div slot="modal-footer" class="w-100"> - <div class="float-right"> - <gl-button @click="cancelDelete()">{{ __('Cancel') }}</gl-button> - <gl-button - ref="modal-delete-button" - data-method="delete" - :to="destroyPath" - variant="danger" - category="primary" - data-qa-selector="delete_modal_button" - @click="track($options.trackingActions.DELETE_PACKAGE)" - > - {{ __('Delete') }} - </gl-button> + <template #modal-footer> + <div class="gl-w-full"> + <div class="float-right"> + <gl-button @click="cancelDelete">{{ __('Cancel') }}</gl-button> + <gl-button + ref="modal-delete-button" + variant="danger" + category="primary" + data-qa-selector="delete_modal_button" + @click="confirmPackageDeletion" + > + {{ __('Delete') }} + </gl-button> + </div> </div> - </div> + </template> </gl-modal> </div> </template> diff --git a/app/assets/javascripts/packages/details/components/code_instruction.vue b/app/assets/javascripts/packages/details/components/code_instruction.vue deleted file mode 100644 index 0719ddfcd2b..00000000000 --- a/app/assets/javascripts/packages/details/components/code_instruction.vue +++ /dev/null @@ -1,63 +0,0 @@ -<script> -import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import Tracking from '~/tracking'; -import { TrackingLabels } from '../constants'; - -export default { - name: 'CodeInstruction', - components: { - ClipboardButton, - }, - mixins: [ - Tracking.mixin({ - label: TrackingLabels.CODE_INSTRUCTION, - }), - ], - props: { - instruction: { - type: String, - required: true, - }, - copyText: { - type: String, - required: true, - }, - multiline: { - type: Boolean, - required: false, - default: false, - }, - trackingAction: { - type: String, - required: false, - default: '', - }, - }, - methods: { - trackCopy() { - if (this.trackingAction) { - this.track(this.trackingAction); - } - }, - }, -}; -</script> - -<template> - <div v-if="!multiline" class="input-group gl-mb-3"> - <input - :value="instruction" - type="text" - class="form-control monospace js-instruction-input" - readonly - @copy="trackCopy" - /> - <span class="input-group-append js-instruction-button" @click="trackCopy"> - <clipboard-button :text="instruction" :title="copyText" class="input-group-text" /> - </span> - </div> - - <div v-else> - <pre class="js-instruction-pre" @copy="trackCopy">{{ instruction }}</pre> - </div> -</template> diff --git a/app/assets/javascripts/packages/details/components/composer_installation.vue b/app/assets/javascripts/packages/details/components/composer_installation.vue index 1934da149ce..60ad468c293 100644 --- a/app/assets/javascripts/packages/details/components/composer_installation.vue +++ b/app/assets/javascripts/packages/details/components/composer_installation.vue @@ -2,8 +2,8 @@ import { GlLink, GlSprintf } from '@gitlab/ui'; import { mapGetters, mapState } from 'vuex'; import { s__ } from '~/locale'; -import CodeInstruction from './code_instruction.vue'; -import { TrackingActions } from '../constants'; +import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; +import { TrackingActions, TrackingLabels } from '../constants'; export default { name: 'ComposerInstallation', @@ -26,28 +26,30 @@ export default { ), }, trackingActions: { ...TrackingActions }, + TrackingLabels, }; </script> <template> <div> <h3 class="gl-font-lg">{{ __('Installation') }}</h3> - <h4 class="gl-font-base" data-testid="registry-include-title"> - {{ $options.i18n.registryInclude }} - </h4> <code-instruction + :label="$options.i18n.registryInclude" :instruction="composerRegistryInclude" :copy-text="$options.i18n.copyRegistryInclude" :tracking-action="$options.trackingActions.COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND" + :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" + data-testid="registry-include" /> - <h4 class="gl-font-base" data-testid="package-include-title"> - {{ $options.i18n.packageInclude }} - </h4> + <code-instruction + :label="$options.i18n.packageInclude" :instruction="composerPackageInclude" :copy-text="$options.i18n.copyPackageInclude" :tracking-action="$options.trackingActions.COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND" + :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" + data-testid="package-include" /> <span data-testid="help-text"> <gl-sprintf :message="$options.i18n.infoLine"> diff --git a/app/assets/javascripts/packages/details/components/conan_installation.vue b/app/assets/javascripts/packages/details/components/conan_installation.vue index cff7d73f1e8..a5df87c9c5b 100644 --- a/app/assets/javascripts/packages/details/components/conan_installation.vue +++ b/app/assets/javascripts/packages/details/components/conan_installation.vue @@ -2,8 +2,8 @@ import { GlLink, GlSprintf } from '@gitlab/ui'; import { mapGetters, mapState } from 'vuex'; import { s__ } from '~/locale'; -import CodeInstruction from './code_instruction.vue'; -import { TrackingActions } from '../constants'; +import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; +import { TrackingActions, TrackingLabels } from '../constants'; export default { name: 'ConanInstallation', @@ -22,30 +22,30 @@ export default { ), }, trackingActions: { ...TrackingActions }, + TrackingLabels, }; </script> <template> <div> <h3 class="gl-font-lg">{{ __('Installation') }}</h3> - <h4 class="gl-font-base"> - {{ s__('PackageRegistry|Conan Command') }} - </h4> <code-instruction + :label="s__('PackageRegistry|Conan Command')" :instruction="conanInstallationCommand" :copy-text="s__('PackageRegistry|Copy Conan Command')" :tracking-action="$options.trackingActions.COPY_CONAN_COMMAND" + :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" /> <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3> - <h4 class="gl-font-base"> - {{ s__('PackageRegistry|Add Conan Remote') }} - </h4> + <code-instruction + :label="s__('PackageRegistry|Add Conan Remote')" :instruction="conanSetupCommand" :copy-text="s__('PackageRegistry|Copy Conan Setup Command')" :tracking-action="$options.trackingActions.COPY_CONAN_SETUP_COMMAND" + :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" /> <gl-sprintf :message="$options.i18n.helpText"> <template #link="{ content }"> diff --git a/app/assets/javascripts/packages/details/components/dependency_row.vue b/app/assets/javascripts/packages/details/components/dependency_row.vue index 788673d2881..1a2202b23c8 100644 --- a/app/assets/javascripts/packages/details/components/dependency_row.vue +++ b/app/assets/javascripts/packages/details/components/dependency_row.vue @@ -26,7 +26,7 @@ export default { <div v-if="showVersion" - class="table-section section-50 gl-display-flex justify-content-md-end" + class="table-section section-50 gl-display-flex gl-md-justify-content-end" data-testid="version-pattern" > <span class="gl-text-body">{{ dependency.version_pattern }}</span> diff --git a/app/assets/javascripts/packages/details/components/maven_installation.vue b/app/assets/javascripts/packages/details/components/maven_installation.vue index d6641c886a0..c2f6f76967b 100644 --- a/app/assets/javascripts/packages/details/components/maven_installation.vue +++ b/app/assets/javascripts/packages/details/components/maven_installation.vue @@ -2,8 +2,8 @@ import { GlLink, GlSprintf } from '@gitlab/ui'; import { mapGetters, mapState } from 'vuex'; import { s__ } from '~/locale'; -import CodeInstruction from './code_instruction.vue'; -import { TrackingActions } from '../constants'; +import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; +import { TrackingActions, TrackingLabels } from '../constants'; export default { name: 'MavenInstallation', @@ -28,6 +28,7 @@ export default { ), }, trackingActions: { ...TrackingActions }, + TrackingLabels, }; </script> @@ -35,9 +36,6 @@ export default { <div> <h3 class="gl-font-lg">{{ __('Installation') }}</h3> - <h4 class="gl-font-base"> - {{ s__('PackageRegistry|Maven XML') }} - </h4> <p> <gl-sprintf :message="$options.i18n.xmlText"> <template #code="{ content }"> @@ -45,20 +43,22 @@ export default { </template> </gl-sprintf> </p> + <code-instruction + :label="s__('PackageRegistry|Maven XML')" :instruction="mavenInstallationXml" :copy-text="s__('PackageRegistry|Copy Maven XML')" multiline :tracking-action="$options.trackingActions.COPY_MAVEN_XML" + :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" /> - <h4 class="gl-font-base"> - {{ s__('PackageRegistry|Maven Command') }} - </h4> <code-instruction + :label="s__('PackageRegistry|Maven Command')" :instruction="mavenInstallationCommand" :copy-text="s__('PackageRegistry|Copy Maven command')" :tracking-action="$options.trackingActions.COPY_MAVEN_COMMAND" + :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" /> <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3> @@ -74,6 +74,7 @@ export default { :copy-text="s__('PackageRegistry|Copy Maven registry XML')" multiline :tracking-action="$options.trackingActions.COPY_MAVEN_SETUP" + :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" /> <gl-sprintf :message="$options.i18n.helpText"> <template #link="{ content }"> diff --git a/app/assets/javascripts/packages/details/components/npm_installation.vue b/app/assets/javascripts/packages/details/components/npm_installation.vue index d7ff8428370..37ba279d098 100644 --- a/app/assets/javascripts/packages/details/components/npm_installation.vue +++ b/app/assets/javascripts/packages/details/components/npm_installation.vue @@ -2,8 +2,8 @@ import { GlLink, GlSprintf } from '@gitlab/ui'; import { mapGetters, mapState } from 'vuex'; import { s__ } from '~/locale'; -import CodeInstruction from './code_instruction.vue'; -import { NpmManager, TrackingActions } from '../constants'; +import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; +import { NpmManager, TrackingActions, TrackingLabels } from '../constants'; export default { name: 'NpmInstallation', @@ -34,41 +34,46 @@ export default { ), }, trackingActions: { ...TrackingActions }, + TrackingLabels, }; </script> <template> <div> <h3 class="gl-font-lg">{{ __('Installation') }}</h3> - <h4 class="gl-font-base">{{ s__('PackageRegistry|npm command') }}</h4> <code-instruction + :label="s__('PackageRegistry|npm command')" :instruction="npmCommand" :copy-text="s__('PackageRegistry|Copy npm command')" :tracking-action="$options.trackingActions.COPY_NPM_INSTALL_COMMAND" + :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" /> - <h4 class="gl-font-base">{{ s__('PackageRegistry|yarn command') }}</h4> <code-instruction + :label="s__('PackageRegistry|yarn command')" :instruction="yarnCommand" :copy-text="s__('PackageRegistry|Copy yarn command')" :tracking-action="$options.trackingActions.COPY_YARN_INSTALL_COMMAND" + :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" /> <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3> - <h4 class="gl-font-base">{{ s__('PackageRegistry|npm command') }}</h4> <code-instruction + :label="s__('PackageRegistry|npm command')" :instruction="npmSetup" :copy-text="s__('PackageRegistry|Copy npm setup command')" :tracking-action="$options.trackingActions.COPY_NPM_SETUP_COMMAND" + :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" /> - <h4 class="gl-font-base">{{ s__('PackageRegistry|yarn command') }}</h4> <code-instruction + :label="s__('PackageRegistry|yarn command')" :instruction="yarnSetupCommand" :copy-text="s__('PackageRegistry|Copy yarn setup command')" :tracking-action="$options.trackingActions.COPY_YARN_SETUP_COMMAND" + :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" /> <gl-sprintf :message="$options.i18n.helpText"> diff --git a/app/assets/javascripts/packages/details/components/nuget_installation.vue b/app/assets/javascripts/packages/details/components/nuget_installation.vue index 150b6e3ab0f..36887703716 100644 --- a/app/assets/javascripts/packages/details/components/nuget_installation.vue +++ b/app/assets/javascripts/packages/details/components/nuget_installation.vue @@ -2,8 +2,8 @@ import { GlLink, GlSprintf } from '@gitlab/ui'; import { mapGetters, mapState } from 'vuex'; import { s__ } from '~/locale'; -import CodeInstruction from './code_instruction.vue'; -import { TrackingActions } from '../constants'; +import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; +import { TrackingActions, TrackingLabels } from '../constants'; export default { name: 'NugetInstallation', @@ -22,29 +22,28 @@ export default { ), }, trackingActions: { ...TrackingActions }, + TrackingLabels, }; </script> <template> <div> <h3 class="gl-font-lg">{{ __('Installation') }}</h3> - <h4 class="gl-font-base"> - {{ s__('PackageRegistry|NuGet Command') }} - </h4> <code-instruction + :label="s__('PackageRegistry|NuGet Command')" :instruction="nugetInstallationCommand" :copy-text="s__('PackageRegistry|Copy NuGet Command')" :tracking-action="$options.trackingActions.COPY_NUGET_INSTALL_COMMAND" + :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" /> <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3> - <h4 class="gl-font-base"> - {{ s__('PackageRegistry|Add NuGet Source') }} - </h4> <code-instruction + :label="s__('PackageRegistry|Add NuGet Source')" :instruction="nugetSetupCommand" :copy-text="s__('PackageRegistry|Copy NuGet Setup Command')" :tracking-action="$options.trackingActions.COPY_NUGET_SETUP_COMMAND" + :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" /> <gl-sprintf :message="$options.i18n.helpText"> <template #link="{ content }"> diff --git a/app/assets/javascripts/packages/details/components/package_history.vue b/app/assets/javascripts/packages/details/components/package_history.vue index 96ce106884d..413ab1d15cb 100644 --- a/app/assets/javascripts/packages/details/components/package_history.vue +++ b/app/assets/javascripts/packages/details/components/package_history.vue @@ -2,7 +2,7 @@ import { GlLink, GlSprintf } from '@gitlab/ui'; import { s__ } from '~/locale'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import HistoryElement from './history_element.vue'; +import HistoryItem from '~/vue_shared/components/registry/history_item.vue'; export default { name: 'PackageHistory', @@ -16,7 +16,7 @@ export default { components: { GlLink, GlSprintf, - HistoryElement, + HistoryItem, TimeAgoTooltip, }, props: { @@ -46,7 +46,7 @@ export default { <div class="issuable-discussion"> <h3 class="gl-font-lg" data-testid="title">{{ __('History') }}</h3> <ul class="timeline main-notes-list notes gl-mb-4" data-testid="timeline"> - <history-element icon="clock" data-testid="created-on"> + <history-item icon="clock" data-testid="created-on"> <gl-sprintf :message="$options.i18n.createdOn"> <template #name> <strong>{{ packageEntity.name }}</strong> @@ -58,8 +58,8 @@ export default { <time-ago-tooltip :time="packageEntity.created_at" /> </template> </gl-sprintf> - </history-element> - <history-element icon="pencil" data-testid="updated-at"> + </history-item> + <history-item icon="pencil" data-testid="updated-at"> <gl-sprintf :message="$options.i18n.updatedAtText"> <template #name> <strong>{{ packageEntity.name }}</strong> @@ -71,9 +71,9 @@ export default { <time-ago-tooltip :time="packageEntity.updated_at" /> </template> </gl-sprintf> - </history-element> + </history-item> <template v-if="packagePipeline"> - <history-element icon="commit" data-testid="commit"> + <history-item icon="commit" data-testid="commit"> <gl-sprintf :message="$options.i18n.commitText"> <template #link> <gl-link :href="packagePipeline.project.commit_url">{{ @@ -84,8 +84,8 @@ export default { <strong>{{ packagePipeline.ref }}</strong> </template> </gl-sprintf> - </history-element> - <history-element icon="pipeline" data-testid="pipeline"> + </history-item> + <history-item icon="pipeline" data-testid="pipeline"> <gl-sprintf :message="$options.i18n.pipelineText"> <template #link> <gl-link :href="packagePipeline.project.pipeline_url" @@ -97,9 +97,9 @@ export default { </template> <template #author>{{ packagePipeline.user.name }}</template> </gl-sprintf> - </history-element> + </history-item> </template> - <history-element icon="package" data-testid="published"> + <history-item icon="package" data-testid="published"> <gl-sprintf :message="$options.i18n.publishText"> <template #project> <strong>{{ projectName }}</strong> @@ -108,7 +108,7 @@ export default { <time-ago-tooltip :time="packageEntity.created_at" /> </template> </gl-sprintf> - </history-element> + </history-item> </ul> </div> </template> diff --git a/app/assets/javascripts/packages/details/components/package_title.vue b/app/assets/javascripts/packages/details/components/package_title.vue index d07883e3e7a..69dd494f11a 100644 --- a/app/assets/javascripts/packages/details/components/package_title.vue +++ b/app/assets/javascripts/packages/details/components/package_title.vue @@ -1,19 +1,21 @@ <script> import { mapState, mapGetters } from 'vuex'; -import { GlAvatar, GlIcon, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; +import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; import PackageTags from '../../shared/components/package_tags.vue'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import timeagoMixin from '~/vue_shared/mixins/timeago'; +import TitleArea from '~/vue_shared/components/registry/title_area.vue'; +import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; import { __ } from '~/locale'; export default { name: 'PackageTitle', components: { - GlAvatar, + TitleArea, GlIcon, - GlLink, GlSprintf, PackageTags, + MetadataItem, }, directives: { GlTooltip: GlTooltipDirective, @@ -36,77 +38,49 @@ export default { </script> <template> - <div class="gl-flex-direction-column"> - <div class="gl-display-flex"> - <gl-avatar - v-if="packageIcon" - :src="packageIcon" - shape="rect" - class="gl-align-self-center gl-mr-4" - data-testid="package-icon" - /> - - <div class="gl-display-flex gl-flex-direction-column"> - <h1 class="gl-font-size-h1 gl-mt-3 gl-mb-2"> - {{ packageEntity.name }} - </h1> + <title-area :title="packageEntity.name" :avatar="packageIcon" data-qa-selector="package_title"> + <template #sub-header> + <gl-icon name="eye" class="gl-mr-3" /> + <gl-sprintf :message="$options.i18n.packageInfo"> + <template #version> + {{ packageEntity.version }} + </template> - <div class="gl-display-flex gl-align-items-center gl-text-gray-500"> - <gl-icon name="eye" class="gl-mr-3" /> - <gl-sprintf :message="$options.i18n.packageInfo"> - <template #version> - {{ packageEntity.version }} - </template> + <template #timeAgo> + <span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)"> + {{ timeFormatted(packageEntity.created_at) }} + </span> + </template> + </gl-sprintf> + </template> - <template #timeAgo> - <span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)"> - {{ timeFormatted(packageEntity.created_at) }} - </span> - </template> - </gl-sprintf> - </div> - </div> - </div> + <template v-if="packageTypeDisplay" #metadata_type> + <metadata-item data-testid="package-type" icon="package" :text="packageTypeDisplay" /> + </template> - <div class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mb-3"> - <div v-if="packageTypeDisplay" class="gl-display-flex gl-align-items-center gl-mr-5"> - <gl-icon name="package" class="gl-text-gray-500 gl-mr-3" /> - <span data-testid="package-type" class="gl-font-weight-bold">{{ packageTypeDisplay }}</span> - </div> + <template #metadata_size> + <metadata-item data-testid="package-size" icon="disk" :text="totalSize" /> + </template> - <div v-if="hasTagsToDisplay" class="gl-display-flex gl-align-items-center gl-mr-5"> - <package-tags :tag-display-limit="1" :tags="packageEntity.tags" /> - </div> + <template v-if="packagePipeline" #metadata_pipeline> + <metadata-item + data-testid="pipeline-project" + icon="review-list" + :text="packagePipeline.project.name" + :link="packagePipeline.project.web_url" + /> + </template> - <div v-if="packagePipeline" class="gl-display-flex gl-align-items-center gl-mr-5"> - <gl-icon name="review-list" class="gl-text-gray-500 gl-mr-3" /> - <gl-link - data-testid="pipeline-project" - :href="packagePipeline.project.web_url" - class="gl-font-weight-bold text-truncate" - > - {{ packagePipeline.project.name }} - </gl-link> - </div> + <template v-if="packagePipeline" #metadata_ref> + <metadata-item data-testid="package-ref" icon="branch" :text="packagePipeline.ref" /> + </template> - <div - v-if="packagePipeline" - data-testid="package-ref" - class="gl-display-flex gl-align-items-center gl-mr-5" - > - <gl-icon name="branch" class="gl-text-gray-500 gl-mr-3" /> - <span - v-gl-tooltip - class="gl-font-weight-bold text-truncate mw-xs" - :title="packagePipeline.ref" - >{{ packagePipeline.ref }}</span - > - </div> + <template v-if="hasTagsToDisplay" #metadata_tags> + <package-tags :tag-display-limit="2" :tags="packageEntity.tags" hide-label /> + </template> - <div class="gl-display-flex gl-align-items-center gl-mr-5"> - <gl-icon name="disk" class="gl-text-gray-500 gl-mr-3" /> - <span data-testid="package-size" class="gl-font-weight-bold">{{ totalSize }}</span> - </div> - </div> - </div> + <template #right-actions> + <slot name="delete-button"></slot> + </template> + </title-area> </template> diff --git a/app/assets/javascripts/packages/details/components/pypi_installation.vue b/app/assets/javascripts/packages/details/components/pypi_installation.vue index f1c619fd6d3..f87be68be48 100644 --- a/app/assets/javascripts/packages/details/components/pypi_installation.vue +++ b/app/assets/javascripts/packages/details/components/pypi_installation.vue @@ -2,8 +2,8 @@ import { GlLink, GlSprintf } from '@gitlab/ui'; import { mapGetters, mapState } from 'vuex'; import { s__ } from '~/locale'; -import CodeInstruction from './code_instruction.vue'; -import { TrackingActions } from '../constants'; +import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; +import { TrackingActions, TrackingLabels } from '../constants'; export default { name: 'PyPiInstallation', @@ -25,6 +25,7 @@ export default { ), }, trackingActions: { ...TrackingActions }, + TrackingLabels, }; </script> @@ -32,15 +33,13 @@ export default { <div> <h3 class="gl-font-lg">{{ __('Installation') }}</h3> - <h4 class="gl-font-base"> - {{ s__('PackageRegistry|Pip Command') }} - </h4> - <code-instruction + :label="s__('PackageRegistry|Pip Command')" :instruction="pypiPipCommand" :copy-text="s__('PackageRegistry|Copy Pip command')" data-testid="pip-command" :tracking-action="$options.trackingActions.COPY_PIP_INSTALL_COMMAND" + :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" /> <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3> @@ -58,6 +57,7 @@ export default { data-testid="pypi-setup-content" multiline :tracking-action="$options.trackingActions.COPY_PYPI_SETUP_COMMAND" + :tracking-label="$options.TrackingLabels.CODE_INSTRUCTION" /> <gl-sprintf :message="$options.i18n.helpText"> <template #link="{ content }"> diff --git a/app/assets/javascripts/packages/details/store/actions.js b/app/assets/javascripts/packages/details/store/actions.js index cda80056e19..340f60258a0 100644 --- a/app/assets/javascripts/packages/details/store/actions.js +++ b/app/assets/javascripts/packages/details/store/actions.js @@ -1,9 +1,10 @@ import Api from '~/api'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { FETCH_PACKAGE_VERSIONS_ERROR } from '../constants'; +import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants'; import * as types from './mutation_types'; -export default ({ commit, state }) => { +export const fetchPackageVersions = ({ commit, state }) => { commit(types.SET_LOADING, true); const { project_id, id } = state.packageEntity; @@ -21,3 +22,13 @@ export default ({ commit, state }) => { commit(types.SET_LOADING, false); }); }; + +export const deletePackage = ({ + state: { + packageEntity: { project_id, id }, + }, +}) => { + return Api.deleteProjectPackage(project_id, id).catch(() => { + createFlash(DELETE_PACKAGE_ERROR_MESSAGE); + }); +}; diff --git a/app/assets/javascripts/packages/details/store/getters.js b/app/assets/javascripts/packages/details/store/getters.js index d1814d506ad..ede6d39bde7 100644 --- a/app/assets/javascripts/packages/details/store/getters.js +++ b/app/assets/javascripts/packages/details/store/getters.js @@ -84,10 +84,10 @@ export const npmSetupCommand = ({ packageEntity, npmPath }) => (type = NpmManage const scope = packageEntity.name.substring(0, packageEntity.name.indexOf('/')); if (type === NpmManager.NPM) { - return `echo ${scope}:registry=${npmPath} >> .npmrc`; + return `echo ${scope}:registry=${npmPath}/ >> .npmrc`; } - return `echo \\"${scope}:registry\\" \\"${npmPath}\\" >> .yarnrc`; + return `echo \\"${scope}:registry\\" \\"${npmPath}/\\" >> .yarnrc`; }; export const nugetInstallationCommand = ({ packageEntity }) => diff --git a/app/assets/javascripts/packages/details/store/index.js b/app/assets/javascripts/packages/details/store/index.js index 9687eb98544..15e17bcfaac 100644 --- a/app/assets/javascripts/packages/details/store/index.js +++ b/app/assets/javascripts/packages/details/store/index.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import Vuex from 'vuex'; -import fetchPackageVersions from './actions'; +import * as actions from './actions'; import * as getters from './getters'; import mutations from './mutations'; @@ -8,9 +8,7 @@ Vue.use(Vuex); export default (initialState = {}) => new Vuex.Store({ - actions: { - fetchPackageVersions, - }, + actions, getters, mutations, state: { diff --git a/app/assets/javascripts/packages/list/components/packages_list.vue b/app/assets/javascripts/packages/list/components/packages_list.vue index b26c6087e14..7067f70a923 100644 --- a/app/assets/javascripts/packages/list/components/packages_list.vue +++ b/app/assets/javascripts/packages/list/components/packages_list.vue @@ -82,11 +82,11 @@ export default { </script> <template> - <div class="d-flex flex-column"> + <div class="gl-display-flex gl-flex-direction-column"> <slot v-if="isListEmpty && !isLoading" name="empty-state"></slot> <div v-else-if="isLoading"> - <packages-list-loader :is-group="isGroupPage" /> + <packages-list-loader /> </div> <template v-else> @@ -106,7 +106,7 @@ export default { :per-page="perPage" :total-items="totalItems" align="center" - class="w-100 mt-2" + class="gl-w-full gl-mt-3" /> <gl-modal 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 ef242ea5f75..6304f723f6a 100644 --- a/app/assets/javascripts/packages/list/components/packages_list_app.vue +++ b/app/assets/javascripts/packages/list/components/packages_list_app.vue @@ -2,11 +2,14 @@ import { mapActions, mapState } from 'vuex'; import { GlEmptyState, GlTab, GlTabs, GlLink, GlSprintf } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; +import createFlash from '~/flash'; import PackageFilter from './packages_filter.vue'; import PackageList from './packages_list.vue'; import PackageSort from './packages_sort.vue'; -import { PACKAGE_REGISTRY_TABS } from '../constants'; +import { PACKAGE_REGISTRY_TABS, DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants'; import PackagesComingSoon from '../coming_soon/packages_coming_soon.vue'; +import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants'; +import { historyReplaceState } from '~/lib/utils/common_utils'; export default { components: { @@ -34,6 +37,7 @@ export default { }, mounted() { this.requestPackagesList(); + this.checkDeleteAlert(); }, methods: { ...mapActions(['requestPackagesList', 'requestDeletePackage', 'setSelectedType']), @@ -64,6 +68,16 @@ export default { return s__('PackageRegistry|There are no packages yet'); }, + checkDeleteAlert() { + const urlParams = new URLSearchParams(window.location.search); + const showAlert = urlParams.get(SHOW_DELETE_SUCCESS_ALERT); + if (showAlert) { + // to be refactored to use gl-alert + createFlash({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, type: 'notice' }); + const cleanUrl = window.location.href.split('?')[0]; + historyReplaceState(cleanUrl); + } + }, }, i18n: { widenFilters: s__('PackageRegistry|To widen your search, change or remove the filters above.'), @@ -77,7 +91,9 @@ export default { <template> <gl-tabs @input="tabChanged"> <template #tabs-end> - <div class="d-flex align-self-center ml-md-auto py-1 py-md-0"> + <div + class="gl-display-flex gl-align-self-center gl-py-2 gl-flex-grow-1 gl-justify-content-end" + > <package-filter class="mr-1" @filter="requestPackagesList" /> <package-sort @sort:changed="requestPackagesList" /> </div> diff --git a/app/assets/javascripts/packages/list/constants.js b/app/assets/javascripts/packages/list/constants.js index 510d04965cb..0ff8c86362d 100644 --- a/app/assets/javascripts/packages/list/constants.js +++ b/app/assets/javascripts/packages/list/constants.js @@ -5,7 +5,6 @@ export const FETCH_PACKAGES_LIST_ERROR_MESSAGE = __( 'Something went wrong while fetching the packages list.', ); export const FETCH_PACKAGE_ERROR_MESSAGE = __('Something went wrong while fetching the package.'); -export const DELETE_PACKAGE_ERROR_MESSAGE = __('Something went wrong while deleting the package.'); export const DELETE_PACKAGE_SUCCESS_MESSAGE = __('Package deleted successfully'); export const DEFAULT_PAGE = 1; diff --git a/app/assets/javascripts/packages/list/stores/actions.js b/app/assets/javascripts/packages/list/stores/actions.js index 0ed24aee2c5..bbc11e3cf13 100644 --- a/app/assets/javascripts/packages/list/stores/actions.js +++ b/app/assets/javascripts/packages/list/stores/actions.js @@ -1,10 +1,10 @@ import Api from '~/api'; import axios from '~/lib/utils/axios_utils'; import { deprecatedCreateFlash as createFlash } from '~/flash'; +import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/shared/constants'; import * as types from './mutation_types'; import { FETCH_PACKAGES_LIST_ERROR_MESSAGE, - DELETE_PACKAGE_ERROR_MESSAGE, DELETE_PACKAGE_SUCCESS_MESSAGE, DEFAULT_PAGE, DEFAULT_PAGE_SIZE, diff --git a/app/assets/javascripts/packages/shared/components/package_list_row.vue b/app/assets/javascripts/packages/shared/components/package_list_row.vue index e000279b794..f93bc51d185 100644 --- a/app/assets/javascripts/packages/shared/components/package_list_row.vue +++ b/app/assets/javascripts/packages/shared/components/package_list_row.vue @@ -1,9 +1,10 @@ <script> -import { GlButton, GlIcon, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; +import { GlButton, GlIcon, GlLink, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui'; import PackageTags from './package_tags.vue'; import PublishMethod from './publish_method.vue'; import { getPackageTypeLabel } from '../utils'; import timeagoMixin from '~/vue_shared/mixins/timeago'; +import ListItem from '~/vue_shared/components/registry/list_item.vue'; export default { name: 'PackageListRow', @@ -12,8 +13,10 @@ export default { GlIcon, GlLink, GlSprintf, + GlTruncate, PackageTags, PublishMethod, + ListItem, }, directives: { GlTooltip: GlTooltipDirective, @@ -59,15 +62,15 @@ export default { </script> <template> - <div class="gl-responsive-table-row" data-qa-selector="packages-row"> - <div class="table-section section-50 d-flex flex-md-column justify-content-between flex-wrap"> - <div class="d-flex align-items-center mr-2"> + <list-item data-qa-selector="package_row"> + <template #left-primary> + <div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0"> <gl-link :href="packageLink" + class="gl-text-body gl-min-w-0" data-qa-selector="package_link" - class="text-dark font-weight-bold mb-md-1" > - {{ packageEntity.name }} + <gl-truncate :text="packageEntity.name" /> </gl-link> <package-tags @@ -78,41 +81,42 @@ export default { :tag-display-limit="1" /> </div> - - <div class="d-flex text-secondary text-truncate mt-md-2"> + </template> + <template #left-secondary> + <div class="gl-display-flex"> <span>{{ packageEntity.version }}</span> - <div v-if="hasPipeline" class="d-none d-md-inline-block ml-1"> + <div v-if="hasPipeline" class="gl-display-none gl-display-sm-flex gl-ml-2"> <gl-sprintf :message="s__('PackageRegistry|published by %{author}')"> <template #author>{{ packageEntity.pipeline.user.name }}</template> </gl-sprintf> </div> - <div v-if="hasProjectLink" class="d-flex align-items-center"> - <gl-icon name="review-list" class="text-secondary ml-2 mr-1" /> + <div v-if="hasProjectLink" class="gl-display-flex gl-align-items-center"> + <gl-icon name="review-list" class="gl-ml-3 gl-mr-2 gl-min-w-0" /> <gl-link + class="gl-text-body gl-min-w-0" data-testid="packages-row-project" :href="`/${packageEntity.project_path}`" - class="text-secondary" - >{{ packageEntity.projectPathName }}</gl-link > + <gl-truncate :text="packageEntity.projectPathName" /> + </gl-link> </div> <div v-if="showPackageType" class="d-flex align-items-center" data-testid="package-type"> - <gl-icon name="package" class="text-secondary ml-2 mr-1" /> + <gl-icon name="package" class="gl-ml-3 gl-mr-2" /> <span>{{ packageType }}</span> </div> </div> - </div> + </template> - <div - class="table-section d-flex flex-md-column justify-content-between align-items-md-end" - :class="disableDelete ? 'section-50' : 'section-40'" - > + <template #right-primary> <publish-method :package-entity="packageEntity" :is-group="isGroup" /> + </template> - <div class="text-secondary order-0 order-md-1 mt-md-2"> + <template #right-secondary> + <span> <gl-sprintf :message="__('Created %{timestamp}')"> <template #timestamp> <span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)"> @@ -120,10 +124,10 @@ export default { </span> </template> </gl-sprintf> - </div> - </div> + </span> + </template> - <div v-if="!disableDelete" class="table-section section-10 d-flex justify-content-end"> + <template v-if="!disableDelete" #right-action> <gl-button data-testid="action-delete" icon="remove" @@ -134,6 +138,6 @@ export default { :disabled="!packageEntity._links.delete_api_path" @click="$emit('packageToDelete', packageEntity)" /> - </div> - </div> + </template> + </list-item> </template> diff --git a/app/assets/javascripts/packages/shared/components/package_tags.vue b/app/assets/javascripts/packages/shared/components/package_tags.vue index 391f53c225b..3d7e233c1ba 100644 --- a/app/assets/javascripts/packages/shared/components/package_tags.vue +++ b/app/assets/javascripts/packages/shared/components/package_tags.vue @@ -80,6 +80,7 @@ export default { data-testid="tagBadge" :class="tagBadgeClass(index)" variant="info" + size="sm" >{{ tag.name }}</gl-badge > @@ -89,7 +90,8 @@ export default { data-testid="moreBadge" variant="muted" :title="moreTagsTooltip" - class="gl-display-none d-md-flex gl-ml-2" + size="sm" + class="gl-display-none gl-display-md-flex gl-ml-2" ><gl-sprintf :message="__('+%{tags} more')"> <template #tags> {{ moreTagsDisplay }} @@ -101,7 +103,7 @@ export default { v-if="moreTagsDisplay && hideLabel" data-testid="moreBadge" variant="muted" - class="d-md-none gl-ml-2" + class="gl-display-md-none gl-ml-2" >{{ tagsDisplay }}</gl-badge > </div> diff --git a/app/assets/javascripts/packages/shared/components/packages_list_loader.vue b/app/assets/javascripts/packages/shared/components/packages_list_loader.vue index cd9ef74d467..efd9f8db908 100644 --- a/app/assets/javascripts/packages/shared/components/packages_list_loader.vue +++ b/app/assets/javascripts/packages/shared/components/packages_list_loader.vue @@ -5,40 +5,13 @@ export default { components: { GlSkeletonLoader, }, - props: { - isGroup: { - type: Boolean, - required: false, - default: false, - }, - }, - computed: { - desktopShapes() { - return this.isGroup ? this.$options.shapes.groups : this.$options.shapes.projects; - }, - desktopHeight() { - return this.isGroup ? 38 : 54; - }, - mobileHeight() { - return this.isGroup ? 160 : 170; - }, - }, - shapes: { - groups: [ - { type: 'rect', width: '100', height: '10', x: '0', y: '15' }, - { type: 'rect', width: '100', height: '10', x: '195', y: '15' }, - { type: 'rect', width: '60', height: '10', x: '475', y: '15' }, - { type: 'rect', width: '60', height: '10', x: '675', y: '15' }, - { type: 'rect', width: '100', height: '10', x: '900', y: '15' }, - ], - projects: [ - { type: 'rect', width: '220', height: '10', x: '0', y: '20' }, - { type: 'rect', width: '60', height: '10', x: '305', y: '20' }, - { type: 'rect', width: '60', height: '10', x: '535', y: '20' }, - { type: 'rect', width: '100', height: '10', x: '760', y: '20' }, - { type: 'rect', width: '30', height: '30', x: '970', y: '10', ref: 'button-loader' }, - ], - }, + shapes: [ + { type: 'rect', width: '220', height: '10', x: '0', y: '20' }, + { type: 'rect', width: '60', height: '10', x: '305', y: '20' }, + { type: 'rect', width: '60', height: '10', x: '535', y: '20' }, + { type: 'rect', width: '100', height: '10', x: '760', y: '20' }, + { type: 'rect', width: '30', height: '30', x: '970', y: '10', ref: 'button-loader' }, + ], rowsToRender: { mobile: 5, desktop: 20, @@ -48,34 +21,35 @@ export default { <template> <div> - <div class="d-xs-flex flex-column d-md-none"> + <div class="gl-flex-direction-column gl-display-sm-none" data-testid="mobile-loader"> <gl-skeleton-loader v-for="index in $options.rowsToRender.mobile" :key="index" :width="500" - :height="mobileHeight" + :height="170" preserve-aspect-ratio="xMinYMax meet" > <rect width="500" height="10" x="0" y="15" rx="4" /> <rect width="500" height="10" x="0" y="45" rx="4" /> <rect width="500" height="10" x="0" y="75" rx="4" /> <rect width="500" height="10" x="0" y="105" rx="4" /> - <rect v-if="isGroup" width="500" height="10" x="0" y="135" rx="4" /> - <rect v-else width="30" height="30" x="470" y="135" rx="4" /> + <rect width="500" height="10" x="0" y="135" rx="4" /> </gl-skeleton-loader> </div> - - <div class="d-none d-md-flex flex-column"> + <div + class="gl-display-none gl-display-sm-flex gl-flex-direction-column" + data-testid="desktop-loader" + > <gl-skeleton-loader v-for="index in $options.rowsToRender.desktop" :key="index" :width="1000" - :height="desktopHeight" + :height="54" preserve-aspect-ratio="xMinYMax meet" > <component :is="r.type" - v-for="(r, rIndex) in desktopShapes" + v-for="(r, rIndex) in $options.shapes" :key="rIndex" rx="4" v-bind="r" diff --git a/app/assets/javascripts/packages/shared/components/publish_method.vue b/app/assets/javascripts/packages/shared/components/publish_method.vue index 1e18562a421..d17e23c4032 100644 --- a/app/assets/javascripts/packages/shared/components/publish_method.vue +++ b/app/assets/javascripts/packages/shared/components/publish_method.vue @@ -36,26 +36,28 @@ export default { </script> <template> - <div class="d-flex align-items-center text-secondary order-1 order-md-0 mb-md-1"> + <div class="gl-display-flex gl-align-items-center"> <template v-if="hasPipeline"> - <gl-icon name="git-merge" class="mr-1" /> - <strong ref="pipeline-ref" class="mr-1 text-dark">{{ packageEntity.pipeline.ref }}</strong> + <gl-icon name="git-merge" class="gl-mr-2" /> + <span data-testid="pipeline-ref" class="gl-mr-2">{{ packageEntity.pipeline.ref }}</span> - <gl-icon name="commit" class="mr-1" /> - <gl-link ref="pipeline-sha" :href="linkToCommit" class="mr-1">{{ packageShaShort }}</gl-link> + <gl-icon name="commit" class="gl-mr-2" /> + <gl-link data-testid="pipeline-sha" :href="linkToCommit" class="gl-mr-2">{{ + packageShaShort + }}</gl-link> <clipboard-button :text="packageEntity.pipeline.sha" :title="__('Copy commit SHA')" - css-class="border-0 text-secondary py-0 px-1" + css-class="gl-border-0 gl-py-0 gl-px-2" /> </template> <template v-else> - <gl-icon name="upload" class="mr-1" /> - <strong ref="manual-ref" class="text-dark">{{ - s__('PackageRegistry|Manually Published') - }}</strong> + <gl-icon name="upload" class="gl-mr-2" /> + <span data-testid="manually-published"> + {{ s__('PackageRegistry|Manually Published') }} + </span> </template> </div> </template> diff --git a/app/assets/javascripts/packages/shared/constants.js b/app/assets/javascripts/packages/shared/constants.js index 279c2959fa9..e5131db59bf 100644 --- a/app/assets/javascripts/packages/shared/constants.js +++ b/app/assets/javascripts/packages/shared/constants.js @@ -1,3 +1,5 @@ +import { __ } from '~/locale'; + export const PackageType = { CONAN: 'conan', MAVEN: 'maven', @@ -22,3 +24,6 @@ export const TrackingCategories = { [PackageType.NPM]: 'NpmPackages', [PackageType.CONAN]: 'ConanPackages', }; + +export const SHOW_DELETE_SUCCESS_ALERT = 'showSuccessDeleteAlert'; +export const DELETE_PACKAGE_ERROR_MESSAGE = __('Something went wrong while deleting the package.'); diff --git a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js index bbaaeb55c65..a2fca238613 100644 --- a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js +++ b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js @@ -1,3 +1,3 @@ -import setup from 'ee_else_ce/admin/application_settings/setup_metrics_and_profiling'; +import setup from '~/admin/application_settings/setup_metrics_and_profiling'; document.addEventListener('DOMContentLoaded', setup); 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 e7b468f039f..f8fc53799a8 100644 --- a/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js +++ b/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js @@ -3,9 +3,9 @@ import { __ } from '../../../locale'; import { deprecatedCreateFlash as flash } from '../../../flash'; export default class PayloadPreviewer { - constructor(trigger, container) { + constructor(trigger) { this.trigger = trigger; - this.container = container; + this.container = document.querySelector(trigger.dataset.payloadSelector); this.isVisible = false; this.isInserted = false; } diff --git a/app/assets/javascripts/pages/admin/clusters/new/index.js b/app/assets/javascripts/pages/admin/clusters/new/index.js new file mode 100644 index 00000000000..876bab0b339 --- /dev/null +++ b/app/assets/javascripts/pages/admin/clusters/new/index.js @@ -0,0 +1,5 @@ +import initNewCluster from '~/clusters/new_cluster'; + +document.addEventListener('DOMContentLoaded', () => { + initNewCluster(); +}); diff --git a/app/assets/javascripts/pages/admin/cohorts/index.js b/app/assets/javascripts/pages/admin/cohorts/index.js new file mode 100644 index 00000000000..1cc54df15a1 --- /dev/null +++ b/app/assets/javascripts/pages/admin/cohorts/index.js @@ -0,0 +1,22 @@ +import Vue from 'vue'; +import UsagePingDisabled from '~/admin/cohorts/components/usage_ping_disabled.vue'; + +document.addEventListener('DOMContentLoaded', () => { + const emptyStateContainer = document.getElementById('js-cohorts-empty-state'); + + if (!emptyStateContainer) return false; + + const { emptyStateSvgPath, enableUsagePingLink, docsLink } = emptyStateContainer.dataset; + + return new Vue({ + el: emptyStateContainer, + provide: { + svgPath: emptyStateSvgPath, + primaryButtonPath: enableUsagePingLink, + docsLink, + }, + render(h) { + return h(UsagePingDisabled); + }, + }); +}); diff --git a/app/assets/javascripts/pages/admin/dev_ops_report/index.js b/app/assets/javascripts/pages/admin/dev_ops_report/index.js new file mode 100644 index 00000000000..643497003ba --- /dev/null +++ b/app/assets/javascripts/pages/admin/dev_ops_report/index.js @@ -0,0 +1,27 @@ +import Vue from 'vue'; +import UserCallout from '~/user_callout'; +import UsagePingDisabled from '~/admin/dev_ops_report/components/usage_ping_disabled.vue'; + +document.addEventListener('DOMContentLoaded', () => { + // eslint-disable-next-line no-new + new UserCallout(); + + const emptyStateContainer = document.getElementById('js-devops-empty-state'); + + if (!emptyStateContainer) return false; + + const { emptyStateSvgPath, enableUsagePingLink, docsLink, isAdmin } = emptyStateContainer.dataset; + + return new Vue({ + el: emptyStateContainer, + provide: { + isAdmin: Boolean(isAdmin), + svgPath: emptyStateSvgPath, + primaryButtonPath: enableUsagePingLink, + docsLink, + }, + render(h) { + return h(UsagePingDisabled); + }, + }); +}); diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue index 8bb093da771..b92fc8d125d 100644 --- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue +++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue @@ -1,11 +1,14 @@ <script> +import { GlSafeHtmlDirective as SafeHtml, GlModal } from '@gitlab/ui'; import { escape } from 'lodash'; -import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; -import { s__, sprintf } from '~/locale'; +import { __, s__, sprintf } from '~/locale'; export default { components: { - DeprecatedModal, + GlModal, + }, + directives: { + SafeHtml, }, props: { deleteProjectUrl: { @@ -62,51 +65,57 @@ export default { false, ); }, - primaryButtonLabel() { - return s__('AdminProjects|Delete project'); - }, canSubmit() { return this.enteredProjectName === this.projectName; }, + primaryProps() { + return { + text: s__('Delete project'), + attributes: [{ variant: 'danger' }, { category: 'primary' }, { disabled: !this.canSubmit }], + }; + }, }, methods: { onCancel() { this.enteredProjectName = ''; }, onSubmit() { + if (!this.canSubmit) { + return; + } this.$refs.form.submit(); this.enteredProjectName = ''; }, }, + cancelProps: { + text: __('Cancel'), + }, }; </script> <template> - <deprecated-modal - id="delete-project-modal" + <gl-modal + modal-id="delete-project-modal" :title="title" - :text="text" - :primary-button-label="primaryButtonLabel" - :submit-disabled="!canSubmit" - kind="danger" - @submit="onSubmit" + :action-primary="primaryProps" + :action-cancel="$options.cancelProps" + :ok-disabled="!canSubmit" + @primary="onSubmit" @cancel="onCancel" > - <template #body="props"> - <p v-html="props.text"></p> - <p v-html="confirmationTextLabel"></p> - <form ref="form" :action="deleteProjectUrl" method="post"> - <input ref="method" type="hidden" name="_method" value="delete" /> - <input :value="csrfToken" type="hidden" name="authenticity_token" /> - <input - v-model="enteredProjectName" - name="projectName" - class="form-control" - type="text" - aria-labelledby="input-label" - autocomplete="off" - /> - </form> - </template> - </deprecated-modal> + <p v-safe-html="text"></p> + <p v-safe-html="confirmationTextLabel"></p> + <form ref="form" :action="deleteProjectUrl" method="post"> + <input ref="method" type="hidden" name="_method" value="delete" /> + <input :value="csrfToken" type="hidden" name="authenticity_token" /> + <input + v-model="enteredProjectName" + name="projectName" + class="form-control" + type="text" + aria-labelledby="input-label" + autocomplete="off" + /> + </form> + </gl-modal> </template> diff --git a/app/assets/javascripts/pages/admin/projects/index/index.js b/app/assets/javascripts/pages/admin/projects/index/index.js index 6fa8760545d..ebb1a74e970 100644 --- a/app/assets/javascripts/pages/admin/projects/index/index.js +++ b/app/assets/javascripts/pages/admin/projects/index/index.js @@ -1,4 +1,3 @@ -import $ from 'jquery'; import Vue from 'vue'; import Translate from '~/vue_shared/translate'; @@ -17,6 +16,18 @@ document.addEventListener('DOMContentLoaded', () => { deleteProjectUrl: '', projectName: '', }, + mounted() { + const deleteProjectButtons = document.querySelectorAll('.delete-project-button'); + deleteProjectButtons.forEach(button => { + button.addEventListener('click', () => { + const buttonProps = button.dataset; + deleteModal.deleteProjectUrl = buttonProps.deleteProjectUrl; + deleteModal.projectName = buttonProps.projectName; + + this.$root.$emit('bv::show::modal', 'delete-project-modal'); + }); + }); + }, render(createElement) { return createElement(deleteProjectModal, { props: { @@ -27,12 +38,4 @@ document.addEventListener('DOMContentLoaded', () => { }); }, }); - - $(document).on('shown.bs.modal', event => { - if (event.relatedTarget.classList.contains('delete-project-button')) { - const buttonProps = event.relatedTarget.dataset; - deleteModal.deleteProjectUrl = buttonProps.deleteProjectUrl; - deleteModal.projectName = buttonProps.projectName; - } - }); }); diff --git a/app/assets/javascripts/pages/admin/services/index/index.js b/app/assets/javascripts/pages/admin/services/index/index.js new file mode 100644 index 00000000000..b2dfbb5a9fc --- /dev/null +++ b/app/assets/javascripts/pages/admin/services/index/index.js @@ -0,0 +1,6 @@ +import PersistentUserCallout from '~/persistent_user_callout'; + +document.addEventListener('DOMContentLoaded', () => { + const callout = document.querySelector('.js-service-templates-deprecated'); + PersistentUserCallout.factory(callout); +}); diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue index e09b8e1bdd5..9c303cc6445 100644 --- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue +++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue @@ -1,6 +1,5 @@ <script> -import { escape } from 'lodash'; -import { GlModal, GlButton, GlFormInput } from '@gitlab/ui'; +import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; export default { @@ -8,6 +7,7 @@ export default { GlModal, GlButton, GlFormInput, + GlSprintf, }, props: { title: { @@ -52,27 +52,6 @@ export default { modalTitle() { return sprintf(this.title, { username: this.username }); }, - text() { - return sprintf( - this.content, - { - username: `<strong>${escape(this.username)}</strong>`, - strong_start: '<strong>', - strong_end: '</strong>', - }, - false, - ); - }, - confirmationTextLabel() { - return sprintf( - s__('AdminUsers|To confirm, type %{username}'), - { - username: `<code>${escape(this.username)}</code>`, - }, - false, - ); - }, - secondaryButtonLabel() { return s__('AdminUsers|Block user'); }, @@ -107,8 +86,25 @@ export default { <template> <gl-modal ref="modal" modal-id="delete-user-modal" :title="modalTitle" kind="danger"> <template> - <p v-html="text"></p> - <p v-html="confirmationTextLabel"></p> + <p> + <gl-sprintf :message="content"> + <template #username> + <strong>{{ username }}</strong> + </template> + <template #strong="props"> + <strong>{{ props.content }}</strong> + </template> + </gl-sprintf> + </p> + + <p> + <gl-sprintf :message="s__('AdminUsers|To confirm, type %{username}')"> + <template #username> + <code>{{ username }}</code> + </template> + </gl-sprintf> + </p> + <form ref="form" :action="deleteUserUrl" method="post" @submit.prevent> <input ref="method" type="hidden" name="_method" value="delete" /> <input :value="csrfToken" type="hidden" name="authenticity_token" /> diff --git a/app/assets/javascripts/pages/admin/users/components/user_operation_confirmation_modal.vue b/app/assets/javascripts/pages/admin/users/components/user_operation_confirmation_modal.vue index 4c335cfb018..4ca6ce6f1c3 100644 --- a/app/assets/javascripts/pages/admin/users/components/user_operation_confirmation_modal.vue +++ b/app/assets/javascripts/pages/admin/users/components/user_operation_confirmation_modal.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { GlModal } from '@gitlab/ui'; import { sprintf } from '~/locale'; diff --git a/app/assets/javascripts/pages/constants.js b/app/assets/javascripts/pages/constants.js index 35c67190b62..a9773807212 100644 --- a/app/assets/javascripts/pages/constants.js +++ b/app/assets/javascripts/pages/constants.js @@ -1,5 +1,3 @@ -/* eslint-disable import/prefer-default-export */ - export const FILTERED_SEARCH = { MERGE_REQUESTS: 'merge_requests', ISSUES: 'issues', 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 6b907f31a37..9fa441348c7 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 @@ -2,11 +2,15 @@ import { GlBanner } from '@gitlab/ui'; import { s__ } from '~/locale'; import axios from '~/lib/utils/axios_utils'; +import Tracking from '~/tracking'; + +const trackingMixin = Tracking.mixin(); export default { components: { GlBanner, }, + mixins: [trackingMixin], inject: { svgPath: { default: '', @@ -20,6 +24,9 @@ export default { calloutsFeatureId: { default: '', }, + trackLabel: { + default: '', + }, }, i18n: { title: s__('CustomizeHomepageBanner|Do you want to customize this page?'), @@ -31,8 +38,19 @@ export default { data() { return { visible: true, + tracking: { + label: this.trackLabel, + }, }; }, + created() { + this.$nextTick(() => { + this.addTrackingAttributesToButton(); + }); + }, + mounted() { + this.trackOnShow(); + }, methods: { handleClose() { axios @@ -45,6 +63,23 @@ export default { }); this.visible = false; + this.track('click_dismiss'); + }, + trackOnShow() { + if (this.visible) this.track('show_home_page_banner'); + }, + addTrackingAttributesToButton() { + // we can't directly add these on the button like we need to due to + // button not being modifiable currently + // https://gitlab.com/gitlab-org/gitlab-ui/-/blob/9209ec424e5cca14bc8a1b5c9fa12636d8c83dad/src/components/base/banner/banner.vue#L60 + const button = this.$refs.banner.$el.querySelector( + `[href='${this.preferencesBehaviorPath}']`, + ); + + if (button) { + button.setAttribute('data-track-event', 'click_go_to_preferences'); + button.setAttribute('data-track-label', this.trackLabel); + } }, }, }; @@ -53,6 +88,7 @@ export default { <template> <gl-banner v-if="visible" + ref="banner" :title="$options.i18n.title" :button-text="$options.i18n.button_text" :button-link="preferencesBehaviorPath" diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js index f76b4b44452..6f8d954d798 100644 --- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js @@ -1,7 +1,7 @@ /* eslint-disable class-methods-use-this, no-unneeded-ternary */ import $ from 'jquery'; -import '~/gl_dropdown'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; import { visitUrl } from '~/lib/utils/url_utility'; import UsersSelect from '~/users_select'; import { isMetaClick } from '~/lib/utils/common_utils'; @@ -50,7 +50,7 @@ export default class Todos { } initFilterDropdown($dropdown, fieldName, searchFields) { - $dropdown.glDropdown({ + initDeprecatedJQueryDropdown($dropdown, { fieldName, selectable: true, filterable: searchFields ? true : false, diff --git a/app/assets/javascripts/pages/groups/clusters/new/index.js b/app/assets/javascripts/pages/groups/clusters/new/index.js new file mode 100644 index 00000000000..876bab0b339 --- /dev/null +++ b/app/assets/javascripts/pages/groups/clusters/new/index.js @@ -0,0 +1,5 @@ +import initNewCluster from '~/clusters/new_cluster'; + +document.addEventListener('DOMContentLoaded', () => { + initNewCluster(); +}); diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js index e146592e134..3fa3a132dfa 100644 --- a/app/assets/javascripts/pages/groups/group_members/index.js +++ b/app/assets/javascripts/pages/groups/group_members/index.js @@ -4,6 +4,7 @@ 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'; function mountRemoveMemberModal() { const el = document.querySelector('.js-remove-member-modal'); @@ -25,6 +26,11 @@ document.addEventListener('DOMContentLoaded', () => { memberExpirationDate('.js-access-expiration-date-groups'); mountRemoveMemberModal(); + initGroupMembersApp(document.querySelector('.js-group-members-list')); + initGroupMembersApp(document.querySelector('.js-group-linked-list')); + initGroupMembersApp(document.querySelector('.js-group-invited-members-list')); + initGroupMembersApp(document.querySelector('.js-group-access-requests-list')); + 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 2496003919a..ae481d16ee9 100644 --- a/app/assets/javascripts/pages/groups/issues/index.js +++ b/app/assets/javascripts/pages/groups/issues/index.js @@ -1,5 +1,5 @@ import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys'; -import initIssuablesList from '~/issuables_list'; +import initIssuablesList from '~/issues_list'; import projectSelect from '~/project_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar'; diff --git a/app/assets/javascripts/pages/groups/shared/group_details.js b/app/assets/javascripts/pages/groups/shared/group_details.js index 37b253d7c48..8546b1f759f 100644 --- a/app/assets/javascripts/pages/groups/shared/group_details.js +++ b/app/assets/javascripts/pages/groups/shared/group_details.js @@ -8,6 +8,7 @@ import NotificationsForm from '~/notifications_form'; import ProjectsList from '~/projects_list'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import GroupTabs from './group_tabs'; +import initInviteMembersBanner from '~/groups/init_invite_members_banner'; export default function initGroupDetails(actionName = 'show') { const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup'); @@ -27,4 +28,5 @@ export default function initGroupDetails(actionName = 'show') { if (newGroupChildWrapper) { new NewGroupChild(newGroupChildWrapper); } + initInviteMembersBanner(); } diff --git a/app/assets/javascripts/pages/instance_statistics/dev_ops_score/index.js b/app/assets/javascripts/pages/instance_statistics/dev_ops_score/index.js deleted file mode 100644 index c1056537f90..00000000000 --- a/app/assets/javascripts/pages/instance_statistics/dev_ops_score/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import UserCallout from '~/user_callout'; - -document.addEventListener('DOMContentLoaded', () => new UserCallout()); 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 5be8e6697a2..983062c79f1 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 @@ -1,4 +1,5 @@ <script> +import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import axios from '~/lib/utils/axios_utils'; import { deprecatedCreateFlash as Flash } from '~/flash'; @@ -11,6 +12,9 @@ export default { components: { DeprecatedModal, }, + directives: { + SafeHtml, + }, props: { issueCount: { type: Number, @@ -124,7 +128,7 @@ Once deleted, it cannot be undone or recovered.`), @submit="onSubmit" > <template #body="props"> - <p v-html="props.text"></p> + <p v-safe-html="props.text"></p> </template> </deprecated-modal> </template> diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js index cb7198e9789..46e59cd6572 100644 --- a/app/assets/javascripts/pages/projects/blob/show/index.js +++ b/app/assets/javascripts/pages/projects/blob/show/index.js @@ -6,6 +6,33 @@ import GpgBadges from '~/gpg_badges'; import '~/sourcegraph/load'; import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue'; +const createGitlabCiYmlVisualization = (containerId = '#js-blob-toggle-graph-preview') => { + const el = document.querySelector(containerId); + const { filename, blobData } = el?.dataset; + + const nameRegexp = /\.gitlab-ci.yml/; + + if (!el || !nameRegexp.test(filename)) { + return; + } + + // eslint-disable-next-line no-new + new Vue({ + el, + components: { + GitlabCiYamlVisualization: () => + import('~/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue'), + }, + render(createElement) { + return createElement('gitlabCiYamlVisualization', { + props: { + blobData, + }, + }); + }, + }); +}; + document.addEventListener('DOMContentLoaded', () => { new BlobViewer(); // eslint-disable-line no-new initBlob(); @@ -63,4 +90,8 @@ document.addEventListener('DOMContentLoaded', () => { }); } } + + if (gon?.features?.gitlabCiYmlPreview) { + createGitlabCiYmlVisualization(); + } }); diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js index 37e8c75f299..623d0a10606 100644 --- a/app/assets/javascripts/pages/projects/branches/index/index.js +++ b/app/assets/javascripts/pages/projects/branches/index/index.js @@ -1,4 +1,4 @@ -import AjaxLoadingSpinner from '~/ajax_loading_spinner'; +import AjaxLoadingSpinner from '~/branches/ajax_loading_spinner'; import DeleteModal from '~/branches/branches_delete_modal'; import initDiverganceGraph from '~/branches/divergence_graph'; diff --git a/app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js b/app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js index 9ab73be80a0..d270bee25c7 100644 --- a/app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js +++ b/app/assets/javascripts/pages/projects/ci/lints/ci_lint_editor.js @@ -1,19 +1,48 @@ +import createFlash from '~/flash'; +import { BLOB_EDITOR_ERROR } from '~/blob_edit/constants'; + export default class CILintEditor { constructor() { - this.editor = window.ace.edit('ci-editor'); - this.textarea = document.querySelector('#content'); + const monacoEnabled = window?.gon?.features?.monacoCi; this.clearYml = document.querySelector('.clear-yml'); - - this.editor.getSession().setMode('ace/mode/yaml'); - this.editor.on('input', () => { - const content = this.editor.getSession().getValue(); - this.textarea.value = content; - }); - this.clearYml.addEventListener('click', this.clear.bind(this)); + + return monacoEnabled ? this.initEditorLite() : this.initAce(); } clear() { this.editor.setValue(''); } + + initEditorLite() { + import(/* webpackChunkName: 'monaco_editor_lite' */ '~/editor/editor_lite') + .then(({ default: EditorLite }) => { + const editorEl = document.getElementById('editor'); + const fileContentEl = document.getElementById('content'); + const form = document.querySelector('.js-ci-lint-form'); + + const rootEditor = new EditorLite(); + + this.editor = rootEditor.createInstance({ + el: editorEl, + blobPath: '.gitlab-ci.yml', + blobContent: editorEl.innerText, + }); + + form.addEventListener('submit', () => { + fileContentEl.value = this.editor.getValue(); + }); + }) + .catch(() => createFlash({ message: BLOB_EDITOR_ERROR })); + } + + initAce() { + this.editor = window.ace.edit('ci-editor'); + this.textarea = document.getElementById('content'); + + this.editor.getSession().setMode('ace/mode/yaml'); + this.editor.on('input', () => { + this.textarea.value = this.editor.getSession().getValue(); + }); + } } diff --git a/app/assets/javascripts/pages/projects/ci/lints/new/index.js b/app/assets/javascripts/pages/projects/ci/lints/new/index.js index 8e8a843da0b..02bfee9810f 100644 --- a/app/assets/javascripts/pages/projects/ci/lints/new/index.js +++ b/app/assets/javascripts/pages/projects/ci/lints/new/index.js @@ -1,3 +1,11 @@ import CILintEditor from '../ci_lint_editor'; +import initCILint from '~/ci_lint/index'; -document.addEventListener('DOMContentLoaded', () => new CILintEditor()); +document.addEventListener('DOMContentLoaded', () => { + if (gon?.features?.ciLintVue) { + initCILint(); + } else { + // eslint-disable-next-line no-new + new CILintEditor(); + } +}); diff --git a/app/assets/javascripts/pages/projects/clusters/new/index.js b/app/assets/javascripts/pages/projects/clusters/new/index.js new file mode 100644 index 00000000000..876bab0b339 --- /dev/null +++ b/app/assets/javascripts/pages/projects/clusters/new/index.js @@ -0,0 +1,5 @@ +import initNewCluster from '~/clusters/new_cluster'; + +document.addEventListener('DOMContentLoaded', () => { + initNewCluster(); +}); diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js index a245af72d93..d5fb2a8be3c 100644 --- a/app/assets/javascripts/pages/projects/commit/show/index.js +++ b/app/assets/javascripts/pages/projects/commit/show/index.js @@ -14,24 +14,23 @@ import axios from '~/lib/utils/axios_utils'; import syntaxHighlight from '~/syntax_highlight'; import flash from '~/flash'; import { __ } from '~/locale'; +import loadAwardsHandler from '~/awards_handler'; document.addEventListener('DOMContentLoaded', () => { const hasPerfBar = document.querySelector('.with-performance-bar'); const performanceHeight = hasPerfBar ? 35 : 0; + initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight); + new ZenMode(); + new ShortcutsNavigation(); + new MiniPipelineGraph({ + container: '.js-commit-pipeline-graph', + }).bindEvents(); + initNotes(); + // eslint-disable-next-line no-jquery/no-load + $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); + fetchCommitMergeRequests(); + const filesContainer = $('.js-diffs-batch'); - const initAfterPageLoad = () => { - new Diff(); - new ZenMode(); - new ShortcutsNavigation(); - new MiniPipelineGraph({ - container: '.js-commit-pipeline-graph', - }).bindEvents(); - initNotes(); - initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight); - // eslint-disable-next-line no-jquery/no-load - $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); - fetchCommitMergeRequests(); - }; if (filesContainer.length) { const batchPath = filesContainer.data('diffFilesPath'); @@ -42,12 +41,13 @@ document.addEventListener('DOMContentLoaded', () => { filesContainer.html($(data.html)); syntaxHighlight(filesContainer); handleLocationHash(); - initAfterPageLoad(); + new Diff(); }) .catch(() => { flash(__('An error occurred while retrieving diff files')); }); } else { - initAfterPageLoad(); + new Diff(); } + loadAwardsHandler(); }); diff --git a/app/assets/javascripts/pages/projects/constants.js b/app/assets/javascripts/pages/projects/constants.js index 9efbf7cd36e..8dc765e5d10 100644 --- a/app/assets/javascripts/pages/projects/constants.js +++ b/app/assets/javascripts/pages/projects/constants.js @@ -1,5 +1,3 @@ -/* eslint-disable import/prefer-default-export */ - export const ISSUABLE_INDEX = { MERGE_REQUEST: 'merge_request_', ISSUE: 'issue_', diff --git a/app/assets/javascripts/pages/projects/environments/show/index.js b/app/assets/javascripts/pages/projects/environments/show/index.js index 10e3e28f024..5d3a153cbd1 100644 --- a/app/assets/javascripts/pages/projects/environments/show/index.js +++ b/app/assets/javascripts/pages/projects/environments/show/index.js @@ -1,3 +1,3 @@ import initShowEnvironment from '~/environments/mount_show'; -document.addEventListener('DOMContentLoaded', () => initShowEnvironment()); +document.addEventListener('DOMContentLoaded', initShowEnvironment); diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue index b4816fa2cb3..57838050d55 100644 --- a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue +++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue @@ -7,6 +7,7 @@ import { GlTooltipDirective, GlTooltip, GlBadge, + GlSafeHtmlDirective as SafeHtml, } from '@gitlab/ui'; import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '~/groups/constants'; import { __ } from '~/locale'; @@ -23,6 +24,7 @@ export default { }, directives: { GlTooltip: GlTooltipDirective, + SafeHtml, }, props: { group: { @@ -119,7 +121,7 @@ export default { </span> </div> <div v-if="group.description" class="description"> - <span v-html="group.markdown_description"> </span> + <span v-safe-html="group.markdown_description"> </span> </div> </div> <div class="gl-display-flex gl-flex-shrink-0"> diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js index 09b440d1413..384216f29eb 100644 --- a/app/assets/javascripts/pages/projects/graphs/charts/index.js +++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js @@ -1,154 +1,155 @@ import Vue from 'vue'; import { GlColumnChart } from '@gitlab/ui/dist/charts'; +import { waitForCSSLoaded } from '../../../../helpers/startup_css_helper'; import { __ } from '~/locale'; import CodeCoverage from '../components/code_coverage.vue'; import SeriesDataMixin from './series_data_mixin'; document.addEventListener('DOMContentLoaded', () => { - const languagesContainer = document.getElementById('js-languages-chart'); - const codeCoverageContainer = document.getElementById('js-code-coverage-chart'); - const monthContainer = document.getElementById('js-month-chart'); - const weekdayContainer = document.getElementById('js-weekday-chart'); - const hourContainer = document.getElementById('js-hour-chart'); + waitForCSSLoaded(() => { + const languagesContainer = document.getElementById('js-languages-chart'); + const codeCoverageContainer = document.getElementById('js-code-coverage-chart'); + const monthContainer = document.getElementById('js-month-chart'); + const weekdayContainer = document.getElementById('js-weekday-chart'); + const hourContainer = document.getElementById('js-hour-chart'); + const LANGUAGE_CHART_HEIGHT = 300; + const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => { + if (firstDayOfWeek === 0) { + return weekDays; + } - const LANGUAGE_CHART_HEIGHT = 300; + return Object.keys(weekDays).reduce((acc, dayName, idx, arr) => { + const reorderedDayName = arr[(idx + firstDayOfWeek) % arr.length]; - const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => { - if (firstDayOfWeek === 0) { - return weekDays; - } + return { + ...acc, + [reorderedDayName]: weekDays[reorderedDayName], + }; + }, {}); + }; - return Object.keys(weekDays).reduce((acc, dayName, idx, arr) => { - const reorderedDayName = arr[(idx + firstDayOfWeek) % arr.length]; - - return { - ...acc, - [reorderedDayName]: weekDays[reorderedDayName], - }; - }, {}); - }; - - // eslint-disable-next-line no-new - new Vue({ - el: languagesContainer, - components: { - GlColumnChart, - }, - data() { - return { - chartData: JSON.parse(languagesContainer.dataset.chartData), - }; - }, - computed: { - seriesData() { - return { full: this.chartData.map(d => [d.label, d.value]) }; + // eslint-disable-next-line no-new + new Vue({ + el: languagesContainer, + components: { + GlColumnChart, }, - }, - render(h) { - return h(GlColumnChart, { - props: { - data: this.seriesData, - xAxisTitle: __('Used programming language'), - yAxisTitle: __('Percentage'), - xAxisType: 'category', - }, - attrs: { - height: LANGUAGE_CHART_HEIGHT, + data() { + return { + chartData: JSON.parse(languagesContainer.dataset.chartData), + }; + }, + computed: { + seriesData() { + return { full: this.chartData.map(d => [d.label, d.value]) }; }, - }); - }, - }); + }, + render(h) { + return h(GlColumnChart, { + props: { + data: this.seriesData, + xAxisTitle: __('Used programming language'), + yAxisTitle: __('Percentage'), + xAxisType: 'category', + }, + attrs: { + height: LANGUAGE_CHART_HEIGHT, + }, + }); + }, + }); - // eslint-disable-next-line no-new - new Vue({ - el: codeCoverageContainer, - render(h) { - return h(CodeCoverage, { - props: { - graphEndpoint: codeCoverageContainer.dataset?.graphEndpoint, - }, - }); - }, - }); + // eslint-disable-next-line no-new + new Vue({ + el: codeCoverageContainer, + render(h) { + return h(CodeCoverage, { + props: { + graphEndpoint: codeCoverageContainer.dataset?.graphEndpoint, + }, + }); + }, + }); - // eslint-disable-next-line no-new - new Vue({ - el: monthContainer, - components: { - GlColumnChart, - }, - mixins: [SeriesDataMixin], - data() { - return { - chartData: JSON.parse(monthContainer.dataset.chartData), - }; - }, - render(h) { - return h(GlColumnChart, { - props: { - data: this.seriesData, - xAxisTitle: __('Day of month'), - yAxisTitle: __('No. of commits'), - xAxisType: 'category', - }, - }); - }, - }); + // eslint-disable-next-line no-new + new Vue({ + el: monthContainer, + components: { + GlColumnChart, + }, + mixins: [SeriesDataMixin], + data() { + return { + chartData: JSON.parse(monthContainer.dataset.chartData), + }; + }, + render(h) { + return h(GlColumnChart, { + props: { + data: this.seriesData, + xAxisTitle: __('Day of month'), + yAxisTitle: __('No. of commits'), + xAxisType: 'category', + }, + }); + }, + }); - // eslint-disable-next-line no-new - new Vue({ - el: weekdayContainer, - components: { - GlColumnChart, - }, - data() { - return { - chartData: JSON.parse(weekdayContainer.dataset.chartData), - }; - }, - computed: { - seriesData() { - const weekDays = reorderWeekDays(this.chartData, gon.first_day_of_week); - const data = Object.keys(weekDays).reduce((acc, key) => { - acc.push([key, weekDays[key]]); - return acc; - }, []); - return { full: data }; + // eslint-disable-next-line no-new + new Vue({ + el: weekdayContainer, + components: { + GlColumnChart, }, - }, - render(h) { - return h(GlColumnChart, { - props: { - data: this.seriesData, - xAxisTitle: __('Weekday'), - yAxisTitle: __('No. of commits'), - xAxisType: 'category', + data() { + return { + chartData: JSON.parse(weekdayContainer.dataset.chartData), + }; + }, + computed: { + seriesData() { + const weekDays = reorderWeekDays(this.chartData, gon.first_day_of_week); + const data = Object.keys(weekDays).reduce((acc, key) => { + acc.push([key, weekDays[key]]); + return acc; + }, []); + return { full: data }; }, - }); - }, - }); + }, + render(h) { + return h(GlColumnChart, { + props: { + data: this.seriesData, + xAxisTitle: __('Weekday'), + yAxisTitle: __('No. of commits'), + xAxisType: 'category', + }, + }); + }, + }); - // eslint-disable-next-line no-new - new Vue({ - el: hourContainer, - components: { - GlColumnChart, - }, - mixins: [SeriesDataMixin], - data() { - return { - chartData: JSON.parse(hourContainer.dataset.chartData), - }; - }, - render(h) { - return h(GlColumnChart, { - props: { - data: this.seriesData, - xAxisTitle: __('Hour (UTC)'), - yAxisTitle: __('No. of commits'), - xAxisType: 'category', - }, - }); - }, + // eslint-disable-next-line no-new + new Vue({ + el: hourContainer, + components: { + GlColumnChart, + }, + mixins: [SeriesDataMixin], + data() { + return { + chartData: JSON.parse(hourContainer.dataset.chartData), + }; + }, + render(h) { + return h(GlColumnChart, { + props: { + data: this.seriesData, + xAxisTitle: __('Hour (UTC)'), + yAxisTitle: __('No. of commits'), + xAxisType: 'category', + }, + }); + }, + }); }); }); diff --git a/app/assets/javascripts/pages/projects/integrations/jira/issues/index/index.js b/app/assets/javascripts/pages/projects/integrations/jira/issues/index/index.js index 260ba69b4bc..534614349bf 100644 --- a/app/assets/javascripts/pages/projects/integrations/jira/issues/index/index.js +++ b/app/assets/javascripts/pages/projects/integrations/jira/issues/index/index.js @@ -1,4 +1,4 @@ -import initIssuablesList from '~/issuables_list'; +import initIssuablesList from '~/issues_list'; document.addEventListener('DOMContentLoaded', () => { initIssuablesList(); diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js index 1711d122080..e1add4a2af3 100644 --- a/app/assets/javascripts/pages/projects/issues/index/index.js +++ b/app/assets/javascripts/pages/projects/issues/index/index.js @@ -7,7 +7,7 @@ import UsersSelect from '~/users_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; import { FILTERED_SEARCH } from '~/pages/constants'; import { ISSUABLE_INDEX } from '~/pages/projects/constants'; -import initIssuablesList from '~/issuables_list'; +import initIssuablesList from '~/issues_list'; import initManualOrdering from '~/manual_ordering'; import { showLearnGitLabIssuesPopover } from '~/onboarding_issues'; diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/index.js b/app/assets/javascripts/pages/projects/issues/service_desk/index.js index 9304d9b6832..e0c1332796f 100644 --- a/app/assets/javascripts/pages/projects/issues/service_desk/index.js +++ b/app/assets/javascripts/pages/projects/issues/service_desk/index.js @@ -1,5 +1,5 @@ import FilteredSearchServiceDesk from './filtered_search'; -import initIssuablesList from '~/issuables_list'; +import initIssuablesList from '~/issues_list'; document.addEventListener('DOMContentLoaded', () => { const supportBotData = JSON.parse( diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js index 5ac6c17e09d..98ae4e26257 100644 --- a/app/assets/javascripts/pages/projects/issues/show.js +++ b/app/assets/javascripts/pages/projects/issues/show.js @@ -1,27 +1,31 @@ +import loadAwardsHandler from '~/awards_handler'; import initIssuableSidebar from '~/init_issuable_sidebar'; import Issue from '~/issue'; import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; import ZenMode from '~/zen_mode'; import '~/notes/index'; import { store } from '~/notes/stores'; -import initIssueableApp from '~/issue_show'; +import initIssueApp from '~/issue_show/issue'; +import initIncidentApp from '~/issue_show/incident'; import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning'; import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace'; import initRelatedMergeRequestsApp from '~/related_merge_requests'; import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle'; +import { parseIssuableData } from '~/issue_show/utils/parse_data'; export default function() { - initIssueableApp(); + const { issueType, ...issuableData } = parseIssuableData(); + + if (issueType === 'incident') { + initIncidentApp(issuableData); + } else { + initIssueApp(issuableData); + } + initIssuableHeaderWarning(store); initSentryErrorStackTraceApp(); initRelatedMergeRequestsApp(); - // This will be removed when we remove the `design_management_moved` feature flag - // See https://gitlab.com/gitlab-org/gitlab/-/issues/223197 - import(/* webpackChunkName: 'design_management' */ '~/design_management_legacy') - .then(module => module.default()) - .catch(() => {}); - import(/* webpackChunkName: 'design_management' */ '~/design_management') .then(module => module.default()) .catch(() => {}); @@ -34,4 +38,6 @@ export default function() { } else { initIssuableSidebar(); } + + loadAwardsHandler(); } diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js index ce74a6de11f..aef4feef42c 100644 --- a/app/assets/javascripts/pages/projects/issues/show/index.js +++ b/app/assets/javascripts/pages/projects/issues/show/index.js @@ -1,4 +1,5 @@ import initSidebarBundle from '~/sidebar/sidebar_bundle'; +import initRelatedIssues from '~/related_issues'; import initShow from '../show'; document.addEventListener('DOMContentLoaded', () => { @@ -6,4 +7,5 @@ document.addEventListener('DOMContentLoaded', () => { if (gon.features && !gon.features.vueIssuableSidebar) { initSidebarBundle(); } + initRelatedIssues(); }); diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js index b72fe6681df..e9f0e008435 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js +++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js @@ -1,8 +1,9 @@ import $ from 'jquery'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default () => { const $targetProjectDropdown = $('.js-target-project'); - $targetProjectDropdown.glDropdown({ + initDeprecatedJQueryDropdown($targetProjectDropdown, { selectable: true, fieldName: $targetProjectDropdown.data('fieldName'), filterable: true, @@ -16,7 +17,7 @@ export default () => { $('.mr_target_commit').empty(); const $targetBranchDropdown = $('.js-target-branch'); $targetBranchDropdown.data('refsUrl', $el.data('refsUrl')); - $targetBranchDropdown.data('glDropdown').clearMenu(); + $targetBranchDropdown.data('deprecatedJQueryDropdown').clearMenu(); }, }); }; 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 25abb80cfae..11af50169f5 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 @@ -6,6 +6,7 @@ import howToMerge from '~/how_to_merge'; import initPipelines from '~/commit/pipelines/pipelines_bundle'; import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle'; import initSourcegraph from '~/sourcegraph'; +import loadAwardsHandler from '~/awards_handler'; export default function() { new ZenMode(); // eslint-disable-line no-new @@ -19,4 +20,5 @@ export default function() { handleLocationHash(); howToMerge(); initSourcegraph(); + loadAwardsHandler(); } diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue index da96e6f36b4..7a3923dfefd 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue @@ -1,6 +1,8 @@ <script> +/* eslint-disable vue/no-v-html */ import Vue from 'vue'; import Cookies from 'js-cookie'; +import { GlIcon } from '@gitlab/ui'; import Translate from '../../../../../vue_shared/translate'; // Full path is needed for Jest to be able to correctly mock this file import illustrationSvg from '~/pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg'; @@ -12,6 +14,9 @@ const cookieKey = 'pipeline_schedules_callout_dismissed'; export default { name: 'PipelineSchedulesCallout', + components: { + GlIcon, + }, data() { return { docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl, @@ -33,7 +38,7 @@ export default { <div v-if="!calloutDismissed" class="pipeline-schedules-user-callout user-callout"> <div class="bordered-box landing content-block"> <button id="dismiss-callout-btn" class="btn btn-default close" @click="dismissCallout"> - <i aria-hidden="true" class="fa fa-times"> </i> + <gl-icon name="close" aria-hidden="true" /> </button> <div class="svg-container" v-html="illustrationSvg"></div> <div class="user-callout-copy"> 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 0057700c1b3..4b203891640 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 @@ -1,4 +1,5 @@ import $ from 'jquery'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default class TargetBranchDropdown { constructor() { @@ -10,7 +11,7 @@ export default class TargetBranchDropdown { } initDropdown() { - this.$dropdown.glDropdown({ + initDeprecatedJQueryDropdown(this.$dropdown, { data: this.formatBranchesList(), filterable: true, selectable: true, 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 a20a0526f12..2a58e015ff1 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 @@ -1,3 +1,5 @@ +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; + const defaultTimezone = { name: 'UTC', offset: 0 }; const defaults = { $inputEl: null, @@ -42,7 +44,7 @@ export default class TimezoneDropdown { } initDropdown() { - this.$dropdown.glDropdown({ + initDeprecatedJQueryDropdown(this.$dropdown, { data: this.timezoneData, filterable: true, selectable: true, diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index bb285635425..2f27814a692 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -8,52 +8,59 @@ import { serializeForm } from '~/lib/utils/forms'; import axios from '~/lib/utils/axios_utils'; import { deprecatedCreateFlash as flash } from '~/flash'; import projectSelect from '../../project_select'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default class Project { constructor() { const $cloneOptions = $('ul.clone-options-dropdown'); - const $projectCloneField = $('#project_clone'); - const $cloneBtnLabel = $('.js-git-clone-holder .js-clone-dropdown-label'); - const mobileCloneField = document.querySelector( - '.js-mobile-git-clone .js-clone-dropdown-label', - ); - - const selectedCloneOption = $cloneBtnLabel.text().trim(); - if (selectedCloneOption.length > 0) { - $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active'); - } + if ($cloneOptions.length) { + const $projectCloneField = $('#project_clone'); + const $cloneBtnLabel = $('.js-git-clone-holder .js-clone-dropdown-label'); + const mobileCloneField = document.querySelector( + '.js-mobile-git-clone .js-clone-dropdown-label', + ); + + const selectedCloneOption = $cloneBtnLabel.text().trim(); + if (selectedCloneOption.length > 0) { + $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active'); + } - $('a', $cloneOptions).on('click', e => { - e.preventDefault(); - const $this = $(e.currentTarget); - const url = $this.attr('href'); - const cloneType = $this.data('cloneType'); - - $('.is-active', $cloneOptions).removeClass('is-active'); - $(`a[data-clone-type="${cloneType}"]`).each(function() { - const $el = $(this); - const activeText = $el.find('.dropdown-menu-inner-title').text(); - const $container = $el.closest('.project-clone-holder'); - const $label = $container.find('.js-clone-dropdown-label'); - - $el.toggleClass('is-active'); - $label.text(activeText); + $('a', $cloneOptions).on('click', e => { + e.preventDefault(); + const $this = $(e.currentTarget); + const url = $this.attr('href'); + const cloneType = $this.data('cloneType'); + + $('.is-active', $cloneOptions).removeClass('is-active'); + $(`a[data-clone-type="${cloneType}"]`).each(function() { + const $el = $(this); + const activeText = $el.find('.dropdown-menu-inner-title').text(); + const $container = $el.closest('.project-clone-holder'); + const $label = $container.find('.js-clone-dropdown-label'); + + $el.toggleClass('is-active'); + $label.text(activeText); + }); + + if (mobileCloneField) { + mobileCloneField.dataset.clipboardText = url; + } else { + $projectCloneField.val(url); + } + $('.js-git-empty .js-clone').text(url); }); + } - if (mobileCloneField) { - mobileCloneField.dataset.clipboardText = url; - } else { - $projectCloneField.val(url); - } - $('.js-git-empty .js-clone').text(url); - }); // Ref switcher - Project.initRefSwitcher(); - $('.project-refs-select').on('change', function() { - return $(this) - .parents('form') - .submit(); - }); + if (document.querySelector('.js-project-refs-dropdown')) { + Project.initRefSwitcher(); + $('.project-refs-select').on('change', function() { + return $(this) + .parents('form') + .submit(); + }); + } + $('.hide-no-ssh-message').on('click', function(e) { Cookies.set('hide_no_ssh_message', 'false'); $(this) @@ -77,6 +84,7 @@ export default class Project { .remove(); return e.preventDefault(); }); + Project.projectSelectDropdown(); } @@ -104,7 +112,7 @@ export default class Project { const action = $form.attr('action'); const linkTarget = mergeUrlParams(serializeForm($form[0]), action); - return $dropdown.glDropdown({ + return initDeprecatedJQueryDropdown($dropdown, { data(term, callback) { axios .get($dropdown.data('refsUrl'), { diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue index 92d23772565..b7546a6bed7 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_setting_row.vue @@ -1,5 +1,10 @@ <script> +import { GlIcon } from '@gitlab/ui'; + export default { + components: { + GlIcon, + }, props: { label: { type: String, @@ -25,7 +30,7 @@ export default { <label v-if="label" class="label-bold"> {{ label }} <a v-if="helpPath" :href="helpPath" target="_blank"> - <i aria-hidden="true" data-hidden="true" class="fa fa-question-circle"> </i> + <gl-icon name="question-o" /> </a> </label> <span v-if="helpText" class="form-text text-muted"> {{ helpText }} </span> <slot></slot> diff --git a/app/assets/javascripts/pages/search/show/index.js b/app/assets/javascripts/pages/search/show/index.js index 85aaaa2c9da..92d01343bd5 100644 --- a/app/assets/javascripts/pages/search/show/index.js +++ b/app/assets/javascripts/pages/search/show/index.js @@ -1,3 +1,7 @@ import Search from './search'; +import initStateFilter from '~/search/state_filter'; -document.addEventListener('DOMContentLoaded', () => new Search()); +document.addEventListener('DOMContentLoaded', () => { + initStateFilter(); + return new Search(); +}); diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js index cc2128490ec..6ff74325a5e 100644 --- a/app/assets/javascripts/pages/search/show/search.js +++ b/app/assets/javascripts/pages/search/show/search.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import '~/gl_dropdown'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; import { deprecatedCreateFlash as Flash } from '~/flash'; import Api from '~/api'; import { __ } from '~/locale'; @@ -20,7 +20,7 @@ export default class Search { this.eventListeners(); refreshCounts(); - $groupDropdown.glDropdown({ + initDeprecatedJQueryDropdown($groupDropdown, { selectable: true, filterable: true, filterRemote: true, @@ -46,7 +46,7 @@ export default class Search { clicked: () => Search.submitSearch(), }); - $projectDropdown.glDropdown({ + initDeprecatedJQueryDropdown($projectDropdown, { selectable: true, filterable: true, filterRemote: true, diff --git a/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue index a7b7d597fb7..653aad3d2f5 100644 --- a/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue +++ b/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue @@ -1,11 +1,12 @@ <script> +import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui'; import { escape } from 'lodash'; -import { GlModal, GlModalDirective } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; export default { components: { GlModal, + GlButton, }, directives: { 'gl-modal': GlModalDirective, @@ -55,14 +56,14 @@ export default { <template> <div class="d-inline-block"> - <button + <gl-button v-gl-modal="modalId" - type="button" - class="btn btn-danger" + category="primary" + variant="danger" data-qa-selector="delete_button" > {{ __('Delete') }} - </button> + </gl-button> <gl-modal :title="title" :action-primary="{ diff --git a/app/assets/javascripts/pdf/page/index.vue b/app/assets/javascripts/pdf/page/index.vue index 65f84e75e86..843c50cf9bc 100644 --- a/app/assets/javascripts/pdf/page/index.vue +++ b/app/assets/javascripts/pdf/page/index.vue @@ -52,19 +52,19 @@ export default { <style> .pdf-page { - margin: 8px auto 0 auto; + margin: 8px auto 0; border-top: 1px #ddd solid; border-bottom: 1px #ddd solid; width: 100%; } .pdf-page:first-child { - margin-top: 0px; - border-top: 0px; + margin-top: 0; + border-top: 0; } .pdf-page:last-child { - margin-bottom: 0px; - border-bottom: 0px; + margin-bottom: 0; + border-bottom: 0; } </style> diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue index ef24dbfb6ce..9f05ee5c7c2 100644 --- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue +++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue @@ -1,14 +1,14 @@ <script> +import { GlIcon } from '@gitlab/ui'; import RequestWarning from './request_warning.vue'; import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue'; -import Icon from '~/vue_shared/components/icon.vue'; export default { components: { RequestWarning, GlModal: DeprecatedModal2, - Icon, + GlIcon, }, props: { currentRequest: { @@ -104,7 +104,7 @@ export default { type="button" :aria-label="__('Toggle backtrace')" > - <icon :size="12" name="ellipsis_h" /> + <gl-icon :size="12" name="ellipsis_h" /> </button> </div> <pre v-if="item.backtrace" class="backtrace-row js-toggle-content mt-2">{{ diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue index cccb5e1be06..165feb1b6aa 100644 --- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue +++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { glEmojiTag } from '~/emoji'; import AddRequest from './add_request.vue'; diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue index c22a648d17f..5a9d3a6d313 100644 --- a/app/assets/javascripts/performance_bar/components/request_selector.vue +++ b/app/assets/javascripts/performance_bar/components/request_selector.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { GlPopover } from '@gitlab/ui'; import { glEmojiTag } from '~/emoji'; import { n__ } from '~/locale'; diff --git a/app/assets/javascripts/performance_bar/components/request_warning.vue b/app/assets/javascripts/performance_bar/components/request_warning.vue index 0128d5bd733..b61e1e5b7a9 100644 --- a/app/assets/javascripts/performance_bar/components/request_warning.vue +++ b/app/assets/javascripts/performance_bar/components/request_warning.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { GlPopover } from '@gitlab/ui'; import { glEmojiTag } from '~/emoji'; diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js index a294f3f36a6..f29b5f42d8f 100644 --- a/app/assets/javascripts/performance_bar/index.js +++ b/app/assets/javascripts/performance_bar/index.js @@ -5,14 +5,17 @@ import axios from '~/lib/utils/axios_utils'; import PerformanceBarService from './services/performance_bar_service'; import PerformanceBarStore from './stores/performance_bar_store'; -export default ({ container }) => - new Vue({ - el: container, +import initPerformanceBarLog from './performance_bar_log'; + +const initPerformanceBar = el => { + const performanceBarData = el.dataset; + + return new Vue({ + el, components: { PerformanceBarApp: () => import('./components/performance_bar_app.vue'), }, data() { - const performanceBarData = document.querySelector(this.$options.el).dataset; const store = new PerformanceBarStore(); return { @@ -24,15 +27,12 @@ export default ({ container }) => }; }, mounted() { - this.interceptor = PerformanceBarService.registerInterceptor( - this.peekUrl, - this.loadRequestDetails, - ); + PerformanceBarService.registerInterceptor(this.peekUrl, this.loadRequestDetails); this.loadRequestDetails(this.requestId, window.location.href); }, beforeDestroy() { - PerformanceBarService.removeInterceptor(this.interceptor); + PerformanceBarService.removeInterceptor(); }, methods: { addRequestManually(urlOrRequestId) { @@ -121,3 +121,15 @@ export default ({ container }) => }); }, }); +}; + +document.addEventListener('DOMContentLoaded', () => { + const jsPeek = document.querySelector('#js-peek'); + if (jsPeek) { + initPerformanceBar(jsPeek); + } +}); + +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 new file mode 100644 index 00000000000..638c544f2e1 --- /dev/null +++ b/app/assets/javascripts/performance_bar/performance_bar_log.js @@ -0,0 +1,28 @@ +/* eslint-disable no-console */ +import { getCLS, getFID, getLCP } from 'web-vitals'; + +const initVitalsLog = () => { + const reportVital = data => { + console.log(`${String.fromCodePoint(0x1f4c8)} ${data.name} : `, data); + }; + + console.log( + `${String.fromCodePoint( + 0x1f4d1, + )} To get the final web vital numbers reported you maybe need to switch away and back to the tab`, + ); + getCLS(reportVital); + getFID(reportVital); + getLCP(reportVital); +}; + +const initPerformanceBarLog = () => { + console.log( + `%c ${String.fromCodePoint(0x1f98a)} GitLab performance bar`, + 'width:100%;background-color: #292961; color: #FFFFFF; font-size:24px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto; padding: 10px;display:block;padding-right: 100px;', + ); + + initVitalsLog(); +}; + +export default initPerformanceBarLog; 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 61b35b4b8f5..3c8303d102e 100644 --- a/app/assets/javascripts/performance_bar/services/performance_bar_service.js +++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js @@ -2,12 +2,14 @@ import axios from '../../lib/utils/axios_utils'; import { parseBoolean } from '~/lib/utils/common_utils'; export default class PerformanceBarService { + static interceptor = null; + static fetchRequestDetails(peekUrl, requestId) { return axios.get(peekUrl, { params: { request_id: requestId } }); } static registerInterceptor(peekUrl, callback) { - const interceptor = response => { + PerformanceBarService.interceptor = response => { const [fireCallback, requestId, requestUrl] = PerformanceBarService.callbackParams( response, peekUrl, @@ -20,22 +22,20 @@ export default class PerformanceBarService { return response; }; - return axios.interceptors.response.use(interceptor); + return axios.interceptors.response.use(PerformanceBarService.interceptor); } - static removeInterceptor(interceptor) { - axios.interceptors.response.eject(interceptor); + static removeInterceptor() { + axios.interceptors.response.eject(PerformanceBarService.interceptor); + PerformanceBarService.interceptor = null; } static callbackParams(response, peekUrl) { const requestId = response.headers && response.headers['x-request-id']; - // Get the request URL from response.config for Axios, and response for - // Vue Resource. - const requestUrl = (response.config || response).url; - const apiRequest = requestUrl && requestUrl.match(/^\/api\//); + const requestUrl = response.config?.url; const cachedResponse = response.headers && parseBoolean(response.headers['x-gitlab-from-cache']); - const fireCallback = requestUrl !== peekUrl && requestId && !apiRequest && !cachedResponse; + const fireCallback = requestUrl !== peekUrl && Boolean(requestId) && !cachedResponse; return [fireCallback, requestId, requestUrl]; } 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 6f443db47ed..8c88851f039 100644 --- a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js +++ b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js @@ -47,7 +47,10 @@ export default class PerformanceBarStore { } canTrackRequest(requestUrl) { - return this.requests.filter(request => request.url === requestUrl).length < 2; + return ( + requestUrl.endsWith('/api/graphql') || + this.requests.filter(request => request.url === requestUrl).length < 2 + ); } static truncateUrl(requestUrl) { 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 e079603a5d4..be8ce832d20 100644 --- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue +++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue @@ -9,13 +9,13 @@ import { GlFormInput, GlFormSelect, GlLink, - GlNewDropdown, - GlNewDropdownItem, + GlDropdown, + GlDropdownItem, GlSearchBoxByType, GlSprintf, } from '@gitlab/ui'; -import { s__, __ } from '~/locale'; -import Api from '~/api'; +import { s__, __, n__ } from '~/locale'; +import axios from '~/lib/utils/axios_utils'; import { redirectTo } from '~/lib/utils/url_utility'; import { VARIABLE_TYPE, FILE_TYPE } from '../constants'; @@ -29,6 +29,8 @@ export default { ), formElementClasses: 'gl-mr-3 gl-mb-3 table-section section-15', errorTitle: __('The form contains the following error:'), + warningTitle: __('The form contains the following warning:'), + maxWarningsSummary: __('%{total} warnings found: showing first %{warningsDisplayed}'), components: { GlAlert, GlButton, @@ -37,8 +39,8 @@ export default { GlFormInput, GlFormSelect, GlLink, - GlNewDropdown, - GlNewDropdownItem, + GlDropdown, + GlDropdownItem, GlSearchBoxByType, GlSprintf, }, @@ -74,13 +76,20 @@ export default { required: false, default: () => ({}), }, + maxWarnings: { + type: Number, + required: true, + }, }, data() { return { searchTerm: '', refValue: this.refParam, variables: {}, - error: false, + error: null, + warnings: [], + totalWarnings: 0, + isWarningDismissed: false, }; }, computed: { @@ -91,6 +100,18 @@ export default { variablesLength() { return Object.keys(this.variables).length; }, + overMaxWarningsLimit() { + return this.totalWarnings > this.maxWarnings; + }, + warningsSummary() { + return n__('%d warning found:', '%d warnings found:', this.warnings.length); + }, + summaryMessage() { + return this.overMaxWarningsLimit ? this.$options.maxWarningsSummary : this.warningsSummary; + }, + shouldShowWarning() { + return this.warnings.length > 0 && !this.isWarningDismissed; + }, }, created() { if (this.variableParams) { @@ -145,13 +166,20 @@ export default { ({ key, value }) => key !== '' && value !== '', ); - return Api.createPipeline(this.projectId, { - ref: this.refValue, - variables: filteredVariables, - }) - .then(({ data }) => redirectTo(data.web_url)) + return axios + .post(this.pipelinesPath, { + ref: this.refValue, + variables: filteredVariables, + }) + .then(({ data }) => { + redirectTo(`${this.pipelinesPath}/${data.id}`); + }) .catch(err => { - this.error = err.response.data.message.base; + const { errors, warnings, total_warnings: totalWarnings } = err.response.data; + const [error] = errors; + this.error = error; + this.warnings = warnings; + this.totalWarnings = totalWarnings; }); }, }, @@ -166,16 +194,45 @@ export default { :dismissible="false" variant="danger" class="gl-mb-4" + data-testid="run-pipeline-error-alert" >{{ error }}</gl-alert > + <gl-alert + v-if="shouldShowWarning" + :title="$options.warningTitle" + variant="warning" + class="gl-mb-4" + data-testid="run-pipeline-warning-alert" + @dismiss="isWarningDismissed = true" + > + <details> + <summary> + <gl-sprintf :message="summaryMessage"> + <template #total> + {{ totalWarnings }} + </template> + <template #warningsDisplayed> + {{ maxWarnings }} + </template> + </gl-sprintf> + </summary> + <p + v-for="(warning, index) in warnings" + :key="`warning-${index}`" + data-testid="run-pipeline-warning" + > + {{ warning }} + </p> + </details> + </gl-alert> <gl-form-group :label="s__('Pipeline|Run for')"> - <gl-new-dropdown :text="refValue" block> + <gl-dropdown :text="refValue" block> <gl-search-box-by-type v-model.trim="searchTerm" :placeholder="__('Search branches and tags')" class="gl-p-2" /> - <gl-new-dropdown-item + <gl-dropdown-item v-for="(ref, index) in filteredRefs" :key="index" class="gl-font-monospace" @@ -184,8 +241,8 @@ export default { @click="setRefSelected(ref)" > {{ ref }} - </gl-new-dropdown-item> - </gl-new-dropdown> + </gl-dropdown-item> + </gl-dropdown> <template #description> <div> diff --git a/app/assets/javascripts/pipeline_new/index.js b/app/assets/javascripts/pipeline_new/index.js index 1c4812c2e0e..f1ea86f8c5f 100644 --- a/app/assets/javascripts/pipeline_new/index.js +++ b/app/assets/javascripts/pipeline_new/index.js @@ -11,6 +11,7 @@ export default () => { fileParam, refNames, settingsLink, + maxWarnings, } = el?.dataset; const variableParams = JSON.parse(varParam); @@ -29,6 +30,7 @@ export default () => { fileParams, refs, settingsLink, + maxWarnings: Number(maxWarnings), }, }); }, diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index 137455bcae1..efa11580c41 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -1,10 +1,9 @@ <script> -import { GlTooltipDirective, GlButton, GlLoadingIcon } from '@gitlab/ui'; +import { GlTooltipDirective, GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui'; import axios from '~/lib/utils/axios_utils'; import { dasherize } from '~/lib/utils/text_utility'; import { __ } from '~/locale'; import { deprecatedCreateFlash as createFlash } from '~/flash'; -import Icon from '~/vue_shared/components/icon.vue'; /** * Renders either a cancel, retry or play icon button and handles the post request @@ -18,7 +17,7 @@ import Icon from '~/vue_shared/components/icon.vue'; */ export default { components: { - Icon, + GlIcon, GlButton, GlLoadingIcon, }, @@ -92,6 +91,6 @@ export default { @click="onClickAction" > <gl-loading-icon v-if="isLoading" class="js-action-icon-loading" /> - <icon v-else :name="actionIcon" /> + <gl-icon v-else :name="actionIcon" class="gl-mr-0!" /> </gl-button> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index f5bf6a6ed34..924cdeebba1 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -44,6 +44,10 @@ export default { return { downstreamMarginTop: null, jobName: null, + pipelineExpanded: { + jobName: '', + expanded: false, + }, }; }, computed: { @@ -120,6 +124,19 @@ export default { setJob(jobName) { this.jobName = jobName; }, + setPipelineExpanded(jobName, expanded) { + if (expanded) { + this.pipelineExpanded = { + jobName, + expanded, + }; + } else { + this.pipelineExpanded = { + expanded, + jobName: '', + }; + } + }, }, }; </script> @@ -181,6 +198,7 @@ export default { :has-triggered-by="hasTriggeredBy" :action="stage.status.action" :job-hovered="jobName" + :pipeline-expanded="pipelineExpanded" @refreshPipelineGraph="refreshPipelineGraph" /> </ul> @@ -193,6 +211,7 @@ export default { graph-position="right" @linkedPipelineClick="handleClickedDownstream" @downstreamHovered="setJob" + @pipelineExpandToggle="setPipelineExpanded" /> <pipeline-graph 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 15c220a554d..11fb2b18e9d 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue @@ -69,9 +69,7 @@ export default { > <ci-icon :status="group.status" /> - <span - class="ci-status-text text-truncate mw-70p gl-pl-1-deprecated-no-really-do-not-use-me d-inline-block align-bottom" - > + <span class="ci-status-text text-truncate mw-70p gl-pl-2 d-inline-block align-bottom"> {{ group.name }} </span> diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue index 4d72cc55b34..0fe0b671273 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_item.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue @@ -31,7 +31,7 @@ import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin'; */ export default { - hoverClass: 'gl-inset-border-1-blue-500', + hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500', components: { ActionComponent, JobNameComponent, @@ -61,6 +61,11 @@ export default { required: false, default: '', }, + pipelineExpanded: { + type: Object, + required: false, + default: () => ({}), + }, }, computed: { boundary() { @@ -101,8 +106,14 @@ export default { hasAction() { return this.job.status && this.job.status.action && this.job.status.action.path; }, + relatedDownstreamHovered() { + return this.job.name === this.jobHovered; + }, + relatedDownstreamExpanded() { + return this.job.name === this.pipelineExpanded.jobName && this.pipelineExpanded.expanded; + }, jobClasses() { - return this.job.name === this.jobHovered + return this.relatedDownstreamHovered || this.relatedDownstreamExpanded ? `${this.$options.hoverClass} ${this.cssClassJobName}` : this.cssClassJobName; }, @@ -121,8 +132,9 @@ export default { v-gl-tooltip="{ boundary, placement: 'bottom' }" :href="status.details_path" :title="tooltipText" - :class="cssClassJobName" + :class="jobClasses" class="js-pipeline-graph-job-link qa-job-link menu-item" + data-testid="job-with-link" > <job-name-component :name="job.name" :status="job.status" /> </gl-link> diff --git a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue index 74a261f35d7..30ba243077e 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue @@ -27,9 +27,7 @@ export default { <template> <span class="ci-job-name-component mw-100"> <ci-icon :status="status" /> - <span - class="ci-status-text text-truncate mw-70p gl-pl-1-deprecated-no-really-do-not-use-me d-inline-block align-bottom" - > + <span class="ci-status-text text-truncate mw-70p gl-pl-2 d-inline-block align-bottom"> {{ name }} </span> </span> diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue index f0a8f9f7ab7..e359fc787c5 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue @@ -1,5 +1,5 @@ <script> -import { GlTooltipDirective, GlButton } from '@gitlab/ui'; +import { GlTooltipDirective, GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui'; import CiStatus from '~/vue_shared/components/ci_icon.vue'; import { __, sprintf } from '~/locale'; @@ -10,6 +10,8 @@ export default { components: { CiStatus, GlButton, + GlLink, + GlLoadingIcon, }, props: { pipeline: { @@ -25,6 +27,11 @@ export default { required: true, }, }, + data() { + return { + expanded: false, + }; + }, computed: { tooltipText() { return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label} @@ -66,11 +73,22 @@ export default { ? sprintf(__('Created by %{job}'), { job: this.pipeline.source_job.name }) : ''; }, + expandedIcon() { + if (this.parentPipeline) { + return this.expanded ? 'angle-right' : 'angle-left'; + } + return this.expanded ? 'angle-left' : 'angle-right'; + }, + expandButtonPosition() { + return this.parentPipeline ? 'gl-left-0 gl-border-r-1!' : 'gl-right-0 gl-border-l-1!'; + }, }, methods: { onClickLinkedPipeline() { this.$root.$emit('bv::hide::tooltip', this.buttonId); + this.expanded = !this.expanded; this.$emit('pipelineClicked', this.$refs.linkedPipeline); + this.$emit('pipelineExpandToggle', this.pipeline.source_job.name, this.expanded); }, hideTooltips() { this.$root.$emit('bv::hide::tooltip'); @@ -88,27 +106,48 @@ export default { <template> <li ref="linkedPipeline" + v-gl-tooltip class="linked-pipeline build" + :title="tooltipText" :class="{ 'downstream-pipeline': isDownstream }" data-qa-selector="child_pipeline" @mouseover="onDownstreamHovered" @mouseleave="onDownstreamHoverLeave" > - <gl-button - :id="buttonId" - v-gl-tooltip - :title="tooltipText" - class="linked-pipeline-content" - data-qa-selector="linked_pipeline_button" - :class="`js-pipeline-expand-${pipeline.id}`" - :loading="pipeline.isLoading" - @click="onClickLinkedPipeline" + <div + class="gl-relative gl-bg-white gl-p-3 gl-border-solid gl-border-gray-100 gl-border-1" + :class="{ 'gl-pl-9': parentPipeline }" > - <ci-status v-if="!pipeline.isLoading" :status="pipelineStatus" css-classes="gl-top-0" /> - <span class="str-truncated"> {{ downstreamTitle }} • #{{ pipeline.id }} </span> + <div class="gl-display-flex"> + <ci-status + v-if="!pipeline.isLoading" + :status="pipelineStatus" + css-classes="gl-top-0 gl-pr-2" + /> + <div v-else class="gl-pr-2"><gl-loading-icon inline /></div> + <div class="gl-display-flex gl-flex-direction-column gl-w-13"> + <span class="gl-text-truncate"> + {{ downstreamTitle }} + </span> + <div class="gl-text-truncate"> + <gl-link class="gl-text-blue-500!" :href="pipeline.path" data-testid="pipelineLink" + >#{{ pipeline.id }}</gl-link + > + </div> + </div> + </div> <div class="gl-pt-2"> <span class="badge badge-primary" data-testid="downstream-pipeline-label">{{ label }}</span> </div> - </gl-button> + <gl-button + :id="buttonId" + class="gl-absolute gl-top-0 gl-bottom-0 gl-shadow-none! gl-rounded-0!" + :class="`js-pipeline-expand-${pipeline.id} ${expandButtonPosition}`" + :icon="expandedIcon" + data-testid="expandPipelineButton" + data-qa-selector="expand_pipeline_button" + @click="onClickLinkedPipeline" + /> + </div> </li> </template> 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 d82885ff8de..3ad28d88345 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue @@ -44,6 +44,9 @@ export default { onDownstreamHovered(jobName) { this.$emit('downstreamHovered', jobName); }, + onPipelineExpandToggle(jobName, expanded) { + this.$emit('pipelineExpandToggle', jobName, expanded); + }, }, }; </script> @@ -65,6 +68,7 @@ export default { :project-id="projectId" @pipelineClicked="onPipelineClick($event, pipeline, index)" @downstreamHovered="onDownstreamHovered" + @pipelineExpandToggle="onPipelineExpandToggle" /> </ul> </div> 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 9de6ba819c2..1453c349f44 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -41,6 +41,11 @@ export default { required: false, default: '', }, + pipelineExpanded: { + type: Object, + required: false, + default: () => ({}), + }, }, computed: { hasAction() { @@ -86,6 +91,7 @@ export default { v-if="group.size === 1" :job="group.jobs[0]" :job-hovered="jobHovered" + :pipeline-expanded="pipelineExpanded" css-class-job-name="build-content" @pipelineActionRequestComplete="pipelineActionRequestComplete" /> diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue new file mode 100644 index 00000000000..3cc76425e2a --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipeline_graph/gitlab_ci_yaml_visualization.vue @@ -0,0 +1,76 @@ +<script> +import { GlTab, GlTabs } from '@gitlab/ui'; +import jsYaml from 'js-yaml'; +import PipelineGraph from './pipeline_graph.vue'; +import { preparePipelineGraphData } from '../../utils'; + +export default { + FILE_CONTENT_SELECTOR: '#blob-content', + EMPTY_FILE_SELECTOR: '.nothing-here-block', + + components: { + GlTab, + GlTabs, + PipelineGraph, + }, + props: { + blobData: { + required: true, + type: String, + }, + }, + data() { + return { + selectedTabIndex: 0, + pipelineData: {}, + }; + }, + computed: { + isVisualizationTab() { + return this.selectedTabIndex === 1; + }, + }, + async created() { + if (this.blobData) { + // The blobData in this case represents the gitlab-ci.yml data + const json = await jsYaml.load(this.blobData); + this.pipelineData = preparePipelineGraphData(json); + } + }, + methods: { + // This is used because the blob page still uses haml, and we can't make + // our haml hide the unused section so we resort to a standard query here. + toggleFileContent({ isFileTab }) { + const el = document.querySelector(this.$options.FILE_CONTENT_SELECTOR); + const emptySection = document.querySelector(this.$options.EMPTY_FILE_SELECTOR); + + const elementToHide = el || emptySection; + + if (!elementToHide) { + return; + } + + // Checking for the current style display prevents user + // from toggling visiblity on and off when clicking on the tab + if (!isFileTab && elementToHide.style.display !== 'none') { + elementToHide.style.display = 'none'; + } + + if (isFileTab && elementToHide.style.display === 'none') { + elementToHide.style.display = 'block'; + } + }, + }, +}; +</script> +<template> + <div> + <div> + <gl-tabs v-model="selectedTabIndex"> + <gl-tab :title="__('File')" @click="toggleFileContent({ isFileTab: true })" /> + <gl-tab :title="__('Visualization')" @click="toggleFileContent({ isFileTab: false })" /> + </gl-tabs> + </div> + <pipeline-graph v-if="isVisualizationTab" :pipeline-data="pipelineData" /> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue new file mode 100644 index 00000000000..19d41b166c3 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue @@ -0,0 +1,24 @@ +<script> +import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; + +export default { + components: { + tooltipOnTruncate, + }, + props: { + jobName: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <tooltip-on-truncate :title="jobName" truncate-target="child" placement="top"> + <div + class="gl-bg-white gl-text-center gl-text-truncate gl-rounded-pill gl-inset-border-1-green-600 gl-mb-3 gl-px-5 gl-py-2 pipeline-job-pill " + > + {{ jobName }} + </div> + </tooltip-on-truncate> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue new file mode 100644 index 00000000000..6a0d3cce1f3 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue @@ -0,0 +1,57 @@ +<script> +import { isEmpty } from 'lodash'; +import { GlAlert } from '@gitlab/ui'; +import JobPill from './job_pill.vue'; +import StagePill from './stage_pill.vue'; + +export default { + components: { + GlAlert, + JobPill, + StagePill, + }, + props: { + pipelineData: { + required: true, + type: Object, + }, + }, + computed: { + isPipelineDataEmpty() { + return isEmpty(this.pipelineData); + }, + emptyClass() { + return !this.isPipelineDataEmpty ? 'gl-py-7' : ''; + }, + }, +}; +</script> +<template> + <div class="gl-display-flex gl-bg-gray-50 gl-px-4 gl-overflow-auto" :class="emptyClass"> + <gl-alert v-if="isPipelineDataEmpty" variant="tip" :dismissible="false"> + {{ __('No content to show') }} + </gl-alert> + <template v-else> + <div + v-for="(stage, index) in pipelineData.stages" + :key="`${stage.name}-${index}`" + class="gl-flex-direction-column" + > + <div + class="gl-display-flex gl-align-items-center gl-bg-white gl-w-full gl-px-8 gl-py-4 gl-mb-5" + :class="{ + 'stage-left-rounded': index === 0, + 'stage-right-rounded': index === pipelineData.stages.length - 1, + }" + > + <stage-pill :stage-name="stage.name" :is-empty="stage.groups.length === 0" /> + </div> + <div + class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full gl-px-8" + > + <job-pill v-for="group in stage.groups" :key="group.name" :job-name="group.name" /> + </div> + </div> + </template> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/stage_pill.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/stage_pill.vue new file mode 100644 index 00000000000..7b2458db725 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipeline_graph/stage_pill.vue @@ -0,0 +1,35 @@ +<script> +import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; + +export default { + components: { + tooltipOnTruncate, + }, + props: { + stageName: { + type: String, + required: true, + }, + isEmpty: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + emptyClass() { + return this.isEmpty ? 'gl-bg-gray-200' : 'gl-bg-gray-600'; + }, + }, +}; +</script> +<template> + <tooltip-on-truncate :title="stageName" truncate-target="child" placement="top"> + <div + class="gl-px-5 gl-py-2 gl-text-white gl-text-center gl-text-truncate gl-rounded-pill pipeline-stage-pill" + :class="emptyClass" + > + {{ stageName }} + </div> + </tooltip-on-truncate> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue b/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue index a66bbb7e5ba..d7b6e033bd1 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue @@ -1,12 +1,10 @@ <script> -import { GlDeprecatedButton } from '@gitlab/ui'; -import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import { GlButton } from '@gitlab/ui'; export default { name: 'PipelineNavControls', components: { - LoadingButton, - GlDeprecatedButton, + GlButton, }, props: { newPipelinePath: { @@ -42,25 +40,27 @@ export default { </script> <template> <div class="nav-controls"> - <gl-deprecated-button + <gl-button v-if="newPipelinePath" :href="newPipelinePath" variant="success" + category="primary" class="js-run-pipeline" > {{ s__('Pipelines|Run Pipeline') }} - </gl-deprecated-button> + </gl-button> - <loading-button + <gl-button v-if="resetCachePath" :loading="isResetCacheButtonLoading" - :label="s__('Pipelines|Clear Runner Caches')" class="js-clear-cache" @click="onClickResetCache" - /> + > + {{ s__('Pipelines|Clear Runner Caches') }} + </gl-button> - <gl-deprecated-button v-if="ciLintPath" :href="ciLintPath" class="js-ci-lint"> + <gl-button v-if="ciLintPath" :href="ciLintPath" class="js-ci-lint"> {{ s__('Pipelines|CI Lint') }} - </gl-deprecated-button> + </gl-button> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue index f604edd8859..43a54090e18 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { isEmpty } from 'lodash'; import { GlLink } from '@gitlab/ui'; import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue'; diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue index 2dfc6485d85..adba86d384b 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue @@ -1,5 +1,6 @@ <script> import { isEqual } from 'lodash'; +import { GlIcon } from '@gitlab/ui'; import { __, s__ } from '~/locale'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import PipelinesService from '../../services/pipelines_service'; @@ -9,7 +10,6 @@ import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue'; import NavigationControls from './nav_controls.vue'; import { getParameterByName } from '~/lib/utils/common_utils'; import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin'; -import Icon from '~/vue_shared/components/icon.vue'; import PipelinesFilteredSearch from './pipelines_filtered_search.vue'; import { validateParams } from '../../utils'; import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../../constants'; @@ -21,7 +21,7 @@ export default { NavigationTabs, NavigationControls, PipelinesFilteredSearch, - Icon, + GlIcon, }, mixins: [pipelinesMixin, CIPaginationMixin, glFeatureFlagsMixin()], props: { @@ -285,8 +285,8 @@ export default { v-if="shouldRenderTabs || shouldRenderButtons" class="top-area scrolling-tabs-container inner-page-scroll-tabs" > - <div class="fade-left"><icon name="chevron-lg-left" :size="12" /></div> - <div class="fade-right"><icon name="chevron-lg-right" :size="12" /></div> + <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> <navigation-tabs v-if="shouldRenderTabs" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue index 098efe68b83..97595e5d2ce 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue @@ -1,10 +1,9 @@ <script> -import { GlDeprecatedButton, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui'; +import { GlTooltipDirective, GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui'; import axios from '~/lib/utils/axios_utils'; import { deprecatedCreateFlash as flash } from '~/flash'; import { s__, __, sprintf } from '~/locale'; import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; -import Icon from '~/vue_shared/components/icon.vue'; import eventHub from '../../event_hub'; export default { @@ -12,9 +11,9 @@ export default { GlTooltip: GlTooltipDirective, }, components: { - Icon, + GlIcon, GlCountdown, - GlDeprecatedButton, + GlButton, GlLoadingIcon, }, props: { @@ -83,29 +82,32 @@ export default { type="button" :disabled="isLoading" class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions" - :title="__('Manual job')" + :title="__('Run manual or delayed jobs')" data-toggle="dropdown" - :aria-label="__('Manual job')" + :aria-label="__('Run manual or delayed jobs')" > - <icon name="play" class="icon-play" /> + <gl-icon name="play" class="icon-play" /> <i class="fa fa-caret-down" aria-hidden="true"></i> <gl-loading-icon v-if="isLoading" /> </button> <ul class="dropdown-menu dropdown-menu-right"> <li v-for="action in actions" :key="action.path"> - <gl-deprecated-button + <gl-button + category="tertiary" :class="{ disabled: isActionDisabled(action) }" :disabled="isActionDisabled(action)" - class="js-pipeline-action-link no-btn btn d-flex align-items-center justify-content-between flex-wrap" + class="js-pipeline-action-link" @click="onClickAction(action)" > - {{ action.name }} - <span v-if="action.scheduled_at"> - <icon name="clock" /> - <gl-countdown :end-date-string="action.scheduled_at" /> - </span> - </gl-deprecated-button> + <div class="d-flex justify-content-between flex-wrap"> + {{ action.name }} + <span v-if="action.scheduled_at"> + <gl-icon name="clock" /> + <gl-countdown :end-date-string="action.scheduled_at" /> + </span> + </div> + </gl-button> </li> </ul> </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 59c066b2683..4a3d134685e 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,13 @@ <script> /* eslint-disable @gitlab/vue-require-i18n-strings */ -import { GlLink, GlTooltipDirective } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlLink, GlTooltipDirective, GlIcon } from '@gitlab/ui'; export default { directives: { GlTooltip: GlTooltipDirective, }, components: { - Icon, + GlIcon, GlLink, }, props: { @@ -29,7 +28,7 @@ export default { data-toggle="dropdown" :aria-label="__('Artifacts')" > - <icon name="download" /> + <gl-icon name="download" /> <i class="fa fa-caret-down" aria-hidden="true"></i> </button> <ul class="dropdown-menu dropdown-menu-right"> 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 f25994a7506..1bdb7d18f04 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 @@ -1,4 +1,5 @@ <script> +import { GlButton } from '@gitlab/ui'; import eventHub from '../../event_hub'; import PipelinesActionsComponent from './pipelines_actions.vue'; import PipelinesArtifactsComponent from './pipelines_artifacts.vue'; @@ -8,8 +9,6 @@ import PipelineUrl from './pipeline_url.vue'; import PipelineTriggerer from './pipeline_triggerer.vue'; import PipelinesTimeago from './time_ago.vue'; import CommitComponent from '~/vue_shared/components/commit.vue'; -import LoadingButton from '~/vue_shared/components/loading_button.vue'; -import Icon from '~/vue_shared/components/icon.vue'; import { PIPELINES_TABLE } from '../../constants'; /** @@ -27,8 +26,7 @@ export default { PipelineTriggerer, CiBadge, PipelinesTimeago, - LoadingButton, - Icon, + GlButton, }, props: { pipeline: { @@ -274,6 +272,7 @@ export default { <ci-badge :status="pipelineStatus" :show-text="!isChildView" + :icon-classes="'gl-vertical-align-middle!'" data-qa-selector="pipeline_commit_status" /> </div> @@ -337,28 +336,30 @@ export default { class="d-md-block" /> - <loading-button + <gl-button v-if="pipeline.flags.retryable" :loading="isRetrying" :disabled="isRetrying" - container-class="js-pipelines-retry-button btn btn-default btn-retry" + class="js-pipelines-retry-button btn-retry" data-qa-selector="pipeline_retry_button" + icon="repeat" + variant="default" + category="secondary" @click="handleRetryClick" - > - <icon name="repeat" /> - </loading-button> + /> - <loading-button + <gl-button v-if="pipeline.flags.cancelable" :loading="isCancelling" :disabled="isCancelling" data-toggle="modal" data-target="#confirmation-modal" - container-class="js-pipelines-cancel-button btn btn-remove" + icon="close" + variant="danger" + category="primary" + class="js-pipelines-cancel-button" @click="handleCancelClick" - > - <icon name="close" /> - </loading-button> + /> </div> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue index d992a4b7752..4045f450104 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue @@ -13,18 +13,17 @@ */ import $ from 'jquery'; -import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; +import { GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import { deprecatedCreateFlash as Flash } from '~/flash'; import axios from '~/lib/utils/axios_utils'; import eventHub from '../../event_hub'; -import Icon from '~/vue_shared/components/icon.vue'; import JobItem from '../graph/job_item.vue'; import { PIPELINES_TABLE } from '../../constants'; export default { components: { - Icon, + GlIcon, JobItem, GlLoadingIcon, }, @@ -170,7 +169,7 @@ export default { @click="onClickStage" > <span :aria-label="stage.title" aria-hidden="true" class="no-pointer-events"> - <icon :name="borderlessIcon" /> + <gl-icon :name="borderlessIcon" /> </span> </button> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue index 8a01e1fe3f5..7d13ee582c6 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue @@ -1,5 +1,5 @@ <script> -import iconTimerSvg from 'icons/_icon_timer.svg'; +import { GlIcon } from '@gitlab/ui'; import '~/lib/utils/datetime_utility'; import tooltip from '~/vue_shared/directives/tooltip'; import timeagoMixin from '~/vue_shared/mixins/timeago'; @@ -8,6 +8,7 @@ export default { directives: { tooltip, }, + components: { GlIcon }, mixins: [timeagoMixin], props: { finishedTime: { @@ -19,11 +20,6 @@ export default { required: true, }, }, - data() { - return { - iconTimerSvg, - }; - }, computed: { hasDuration() { return this.duration > 0; @@ -59,11 +55,12 @@ export default { <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Duration') }}</div> <div class="table-mobile-content"> <p v-if="hasDuration" class="duration"> - <span v-html="iconTimerSvg"> </span> {{ durationFormatted }} + <gl-icon name="timer" class="gl-vertical-align-baseline!" aria-hidden="true" /> + {{ durationFormatted }} </p> <p v-if="hasFinishedTime" class="finished-at d-none d-sm-none d-md-block"> - <i class="fa fa-calendar" aria-hidden="true"> </i> + <gl-icon name="calendar" class="gl-vertical-align-baseline!" aria-hidden="true" /> <time v-tooltip diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue index bc1d22e2976..c3398e90895 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue @@ -55,13 +55,14 @@ export default { <template> <div v-if="isLoading"> - <gl-loading-icon size="lg" class="gl-mt-3 js-loading-spinner" /> + <gl-loading-icon size="lg" class="gl-mt-3" /> </div> <div v-else-if="!isLoading && showTests" ref="container" - class="tests-detail position-relative js-tests-detail" + class="tests-detail position-relative" + data-testid="tests-detail" > <transition name="slide" @@ -85,7 +86,7 @@ export default { <div v-else> <div class="row gl-mt-3"> <div class="col-12"> - <p class="js-no-tests-to-show">{{ s__('TestReports|There are no tests to show.') }}</p> + <p data-testid="no-tests-to-show">{{ s__('TestReports|There are no tests to show.') }}</p> </div> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue index 478073e44d1..aa53c5040e8 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue @@ -1,15 +1,12 @@ <script> import { mapGetters } from 'vuex'; -import { GlTooltipDirective, GlFriendlyWrap } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlTooltipDirective, GlFriendlyWrap, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; -import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue'; export default { name: 'TestsSuiteTable', components: { - Icon, - SmartVirtualList, + GlIcon, GlFriendlyWrap, }, directives: { @@ -28,8 +25,6 @@ export default { return this.getSuiteTests.length > 0; }, }, - maxShownRows: 30, - typicalRowHeight: 75, wrapSymbols: ['::', '#', '.', '_', '-', '/', '\\'], }; </script> @@ -61,66 +56,60 @@ export default { </div> </div> - <smart-virtual-list - :length="getSuiteTests.length" - :remain="$options.maxShownRows" - :size="$options.typicalRowHeight" + <div + v-for="(testCase, index) in getSuiteTests" + :key="index" + class="gl-responsive-table-row rounded align-items-md-start mt-xs-3 js-case-row" > - <div - v-for="(testCase, index) in getSuiteTests" - :key="index" - class="gl-responsive-table-row rounded align-items-md-start mt-xs-3 js-case-row" - > - <div class="table-section section-20 section-wrap"> - <div role="rowheader" class="table-mobile-header">{{ __('Suite') }}</div> - <div class="table-mobile-content pr-md-1 gl-overflow-wrap-break"> - <gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.classname" /> - </div> + <div class="table-section section-20 section-wrap"> + <div role="rowheader" class="table-mobile-header">{{ __('Suite') }}</div> + <div class="table-mobile-content pr-md-1 gl-overflow-wrap-break"> + <gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.classname" /> </div> + </div> - <div class="table-section section-20 section-wrap"> - <div role="rowheader" class="table-mobile-header">{{ __('Name') }}</div> - <div class="table-mobile-content pr-md-1 gl-overflow-wrap-break"> - <gl-friendly-wrap - data-testid="caseName" - :symbols="$options.wrapSymbols" - :text="testCase.name" - /> - </div> + <div class="table-section section-20 section-wrap"> + <div role="rowheader" class="table-mobile-header">{{ __('Name') }}</div> + <div class="table-mobile-content pr-md-1 gl-overflow-wrap-break"> + <gl-friendly-wrap + data-testid="caseName" + :symbols="$options.wrapSymbols" + :text="testCase.name" + /> </div> + </div> - <div class="table-section section-10 section-wrap"> - <div role="rowheader" class="table-mobile-header">{{ __('Status') }}</div> - <div class="table-mobile-content text-center"> - <div - class="add-border ci-status-icon d-flex align-items-center justify-content-end justify-content-md-center" - :class="`ci-status-icon-${testCase.status}`" - > - <icon :size="24" :name="testCase.icon" /> - </div> + <div class="table-section section-10 section-wrap"> + <div role="rowheader" class="table-mobile-header">{{ __('Status') }}</div> + <div class="table-mobile-content text-center"> + <div + class="add-border ci-status-icon d-flex align-items-center justify-content-end justify-content-md-center" + :class="`ci-status-icon-${testCase.status}`" + > + <gl-icon :size="24" :name="testCase.icon" /> </div> </div> + </div> - <div class="table-section flex-grow-1"> - <div role="rowheader" class="table-mobile-header">{{ __('Trace'), }}</div> - <div class="table-mobile-content"> - <pre - v-if="testCase.system_output" - class="build-trace build-trace-rounded text-left" - ><code class="bash p-0">{{testCase.system_output}}</code></pre> - </div> + <div class="table-section flex-grow-1"> + <div role="rowheader" class="table-mobile-header">{{ __('Trace'), }}</div> + <div class="table-mobile-content"> + <pre + v-if="testCase.system_output" + class="build-trace build-trace-rounded text-left" + ><code class="bash p-0">{{testCase.system_output}}</code></pre> </div> + </div> - <div class="table-section section-10 section-wrap"> - <div role="rowheader" class="table-mobile-header"> - {{ __('Duration') }} - </div> - <div class="table-mobile-content text-right pr-sm-1"> - {{ testCase.formattedTime }} - </div> + <div class="table-section section-10 section-wrap"> + <div role="rowheader" class="table-mobile-header"> + {{ __('Duration') }} + </div> + <div class="table-mobile-content text-right pr-sm-1"> + {{ testCase.formattedTime }} </div> </div> - </smart-virtual-list> + </div> </div> <div v-else> diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue index 712ac5eb0e5..d33d4e7dfd0 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue @@ -1,15 +1,13 @@ <script> -import { GlDeprecatedButton, GlProgressBar } from '@gitlab/ui'; +import { GlButton, GlProgressBar } from '@gitlab/ui'; import { __ } from '~/locale'; -import { formatTime, secondsToMilliseconds } from '~/lib/utils/datetime_utility'; -import Icon from '~/vue_shared/components/icon.vue'; +import { formattedTime } from '../../stores/test_reports/utils'; export default { name: 'TestSummary', components: { - GlDeprecatedButton, + GlButton, GlProgressBar, - Icon, }, props: { report: { @@ -39,7 +37,7 @@ export default { return 0; }, formattedDuration() { - return formatTime(secondsToMilliseconds(this.report.total_time)); + return formattedTime(this.report.total_time); }, progressBarVariant() { if (this.successPercentage < 33) { @@ -69,14 +67,13 @@ export default { <div> <div class="row"> <div class="col-12 d-flex gl-mt-3 align-items-center"> - <gl-deprecated-button + <gl-button v-if="showBack" - size="sm" + size="small" class="gl-mr-3 js-back-button" + icon="angle-left" @click="onBackClick" - > - <icon name="angle-left" /> - </gl-deprecated-button> + /> <h4>{{ heading }}</h4> </div> diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue index e774fe06fbe..5f9c0be3ccc 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue @@ -2,13 +2,11 @@ import { mapGetters } from 'vuex'; import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { s__ } from '~/locale'; -import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue'; export default { name: 'TestsSummaryTable', components: { GlIcon, - SmartVirtualList, }, directives: { GlTooltip: GlTooltipDirective, @@ -31,8 +29,6 @@ export default { this.$emit('row-click', index); }, }, - maxShownRows: 20, - typicalRowHeight: 55, }; </script> @@ -69,83 +65,77 @@ export default { </div> </div> - <smart-virtual-list - :length="getTestSuites.length" - :remain="$options.maxShownRows" - :size="$options.typicalRowHeight" + <div + v-for="(testSuite, index) in getTestSuites" + :key="index" + role="row" + class="gl-responsive-table-row test-reports-summary-row rounded js-suite-row" + :class="{ + 'gl-responsive-table-row-clickable cursor-pointer': !testSuite.suite_error, + }" + @click="tableRowClick(index)" > - <div - v-for="(testSuite, index) in getTestSuites" - :key="index" - role="row" - class="gl-responsive-table-row test-reports-summary-row rounded js-suite-row" - :class="{ - 'gl-responsive-table-row-clickable cursor-pointer': !testSuite.suite_error, - }" - @click="tableRowClick(index)" - > - <div class="table-section section-25"> - <div role="rowheader" class="table-mobile-header font-weight-bold"> - {{ __('Suite') }} - </div> - <div class="table-mobile-content underline cgray pl-3"> - {{ testSuite.name }} - <gl-icon - v-if="testSuite.suite_error" - ref="suiteErrorIcon" - v-gl-tooltip - name="error" - :title="testSuite.suite_error" - class="vertical-align-middle" - /> - </div> + <div class="table-section section-25"> + <div role="rowheader" class="table-mobile-header font-weight-bold"> + {{ __('Suite') }} </div> + <div class="table-mobile-content underline cgray pl-3"> + {{ testSuite.name }} + <gl-icon + v-if="testSuite.suite_error" + ref="suiteErrorIcon" + v-gl-tooltip + name="error" + :title="testSuite.suite_error" + class="vertical-align-middle" + /> + </div> + </div> - <div class="table-section section-25"> - <div role="rowheader" class="table-mobile-header font-weight-bold"> - {{ __('Duration') }} - </div> - <div class="table-mobile-content text-md-left"> - {{ testSuite.formattedTime }} - </div> + <div class="table-section section-25"> + <div role="rowheader" class="table-mobile-header font-weight-bold"> + {{ __('Duration') }} + </div> + <div class="table-mobile-content text-md-left"> + {{ testSuite.formattedTime }} </div> + </div> - <div class="table-section section-10 text-center"> - <div role="rowheader" class="table-mobile-header font-weight-bold"> - {{ __('Failed') }} - </div> - <div class="table-mobile-content">{{ testSuite.failed_count }}</div> + <div class="table-section section-10 text-center"> + <div role="rowheader" class="table-mobile-header font-weight-bold"> + {{ __('Failed') }} </div> + <div class="table-mobile-content">{{ testSuite.failed_count }}</div> + </div> - <div class="table-section section-10 text-center"> - <div role="rowheader" class="table-mobile-header font-weight-bold"> - {{ __('Errors') }} - </div> - <div class="table-mobile-content">{{ testSuite.error_count }}</div> + <div class="table-section section-10 text-center"> + <div role="rowheader" class="table-mobile-header font-weight-bold"> + {{ __('Errors') }} </div> + <div class="table-mobile-content">{{ testSuite.error_count }}</div> + </div> - <div class="table-section section-10 text-center"> - <div role="rowheader" class="table-mobile-header font-weight-bold"> - {{ __('Skipped') }} - </div> - <div class="table-mobile-content">{{ testSuite.skipped_count }}</div> + <div class="table-section section-10 text-center"> + <div role="rowheader" class="table-mobile-header font-weight-bold"> + {{ __('Skipped') }} </div> + <div class="table-mobile-content">{{ testSuite.skipped_count }}</div> + </div> - <div class="table-section section-10 text-center"> - <div role="rowheader" class="table-mobile-header font-weight-bold"> - {{ __('Passed') }} - </div> - <div class="table-mobile-content">{{ testSuite.success_count }}</div> + <div class="table-section section-10 text-center"> + <div role="rowheader" class="table-mobile-header font-weight-bold"> + {{ __('Passed') }} </div> + <div class="table-mobile-content">{{ testSuite.success_count }}</div> + </div> - <div class="table-section section-10 text-right pr-md-3"> - <div role="rowheader" class="table-mobile-header font-weight-bold"> - {{ __('Total') }} - </div> - <div class="table-mobile-content">{{ testSuite.total_count }}</div> + <div class="table-section section-10 text-right pr-md-3"> + <div role="rowheader" class="table-mobile-header font-weight-bold"> + {{ __('Total') }} </div> + <div class="table-mobile-content">{{ testSuite.total_count }}</div> </div> - </smart-virtual-list> + </div> </div> <div v-else> diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index c57be7c75b0..745f5b886a5 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -14,10 +14,20 @@ import createTestReportsStore from './stores/test_reports'; Vue.use(Translate); +const SELECTORS = { + PIPELINE_DETAILS: '.js-pipeline-details-vue', + PIPELINE_GRAPH: '#js-pipeline-graph-vue', + PIPELINE_HEADER: '#js-pipeline-header-vue', + PIPELINE_TESTS: '#js-pipeline-tests-detail', +}; + const createPipelinesDetailApp = mediator => { + if (!document.querySelector(SELECTORS.PIPELINE_GRAPH)) { + return; + } // eslint-disable-next-line no-new new Vue({ - el: '#js-pipeline-graph-vue', + el: SELECTORS.PIPELINE_GRAPH, components: { pipelineGraph, }, @@ -47,9 +57,12 @@ const createPipelinesDetailApp = mediator => { }; const createPipelineHeaderApp = mediator => { + if (!document.querySelector(SELECTORS.PIPELINE_HEADER)) { + return; + } // eslint-disable-next-line no-new new Vue({ - el: '#js-pipeline-header-vue', + el: SELECTORS.PIPELINE_HEADER, components: { pipelineHeader, }, @@ -93,9 +106,8 @@ const createPipelineHeaderApp = mediator => { }; const createTestDetails = () => { - const el = document.querySelector('#js-pipeline-tests-detail'); + const el = document.querySelector(SELECTORS.PIPELINE_TESTS); const { summaryEndpoint, suiteEndpoint } = el?.dataset || {}; - const testReportsStore = createTestReportsStore({ summaryEndpoint, suiteEndpoint, @@ -115,7 +127,7 @@ const createTestDetails = () => { }; export default () => { - const { dataset } = document.querySelector('.js-pipeline-details-vue'); + const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS); const mediator = new PipelinesMediator({ endpoint: dataset.endpoint }); mediator.fetchPipeline(); diff --git a/app/assets/javascripts/pipelines/stores/test_reports/getters.js b/app/assets/javascripts/pipelines/stores/test_reports/getters.js index 6c670806cc4..c123014756d 100644 --- a/app/assets/javascripts/pipelines/stores/test_reports/getters.js +++ b/app/assets/javascripts/pipelines/stores/test_reports/getters.js @@ -1,4 +1,4 @@ -import { addIconStatus, formattedTime, sortTestCases } from './utils'; +import { addIconStatus, formattedTime } from './utils'; export const getTestSuites = state => { const { test_suites: testSuites = [] } = state.testReports; @@ -14,5 +14,5 @@ export const getSelectedSuite = state => export const getSuiteTests = state => { const { test_cases: testCases = [] } = getSelectedSuite(state); - return testCases.sort(sortTestCases).map(addIconStatus); + return testCases.map(addIconStatus); }; diff --git a/app/assets/javascripts/pipelines/stores/test_reports/utils.js b/app/assets/javascripts/pipelines/stores/test_reports/utils.js index 16fa6935cbe..8f1ac305cda 100644 --- a/app/assets/javascripts/pipelines/stores/test_reports/utils.js +++ b/app/assets/javascripts/pipelines/stores/test_reports/utils.js @@ -1,5 +1,4 @@ -import { TestStatus } from '~/pipelines/constants'; -import { formatTime, secondsToMilliseconds } from '~/lib/utils/datetime_utility'; +import { __, sprintf } from '../../../locale'; export function iconForTestStatus(status) { switch (status) { @@ -12,25 +11,16 @@ export function iconForTestStatus(status) { } } -export const formattedTime = timeInSeconds => formatTime(secondsToMilliseconds(timeInSeconds)); +export const formattedTime = (seconds = 0) => { + if (seconds < 1) { + const milliseconds = seconds * 1000; + return sprintf(__('%{milliseconds}ms'), { milliseconds: milliseconds.toFixed(2) }); + } + return sprintf(__('%{seconds}s'), { seconds: seconds.toFixed(2) }); +}; export const addIconStatus = testCase => ({ ...testCase, icon: iconForTestStatus(testCase.status), formattedTime: formattedTime(testCase.execution_time), }); - -export const sortTestCases = (a, b) => { - if (a.status === b.status) { - return 0; - } - - switch (b.status) { - case TestStatus.SUCCESS: - return -1; - case TestStatus.FAILED: - return 1; - default: - return 0; - } -}; diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js index 2e08001f6b3..bd53b22784c 100644 --- a/app/assets/javascripts/pipelines/utils.js +++ b/app/assets/javascripts/pipelines/utils.js @@ -1,7 +1,49 @@ import { pickBy } from 'lodash'; import { SUPPORTED_FILTER_PARAMETERS } from './constants'; -// eslint-disable-next-line import/prefer-default-export export const validateParams = params => { return pickBy(params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val); }; + +/** + * This function takes a json payload that comes from a yml + * file converted to json through `jsyaml` library. Because we + * naively convert the entire yaml to json, some keys (like `includes`) + * are irrelevant to rendering the graph and must be removed. We also + * restructure the data to have the structure from an API response for the + * pipeline data. + * @param {Object} jsonData + * @returns {Array} - Array of stages containing all jobs + */ +export const preparePipelineGraphData = jsonData => { + const jsonKeys = Object.keys(jsonData); + const jobNames = jsonKeys.filter(job => jsonData[job]?.stage); + + // We merge both the stages from the "stages" key in the yaml and the stage associated + // with each job to show the user both the stages they explicitly defined, and those + // that they added under jobs. We also remove duplicates. + const jobStages = jobNames.map(job => jsonData[job].stage); + const userDefinedStages = jsonData?.stages ?? []; + + // The order is important here. We always show the stages in order they were + // defined in the `stages` key first, and then stages that are under the jobs. + const stages = Array.from(new Set([...userDefinedStages, ...jobStages])); + + const arrayOfJobsByStage = stages.map(val => { + return jobNames.filter(job => { + return jsonData[job].stage === val; + }); + }); + + const pipelineData = stages.map((stage, index) => { + const stageJobs = arrayOfJobsByStage[index]; + return { + name: stage, + groups: stageJobs.map(job => { + return { name: job, jobs: [{ ...jsonData[job] }] }; + }), + }; + }); + + return { stages: pipelineData }; +}; diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue index 605859cfb6a..f06dc72d365 100644 --- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue +++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue @@ -1,11 +1,12 @@ <script> -import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; +/* eslint-disable vue/no-v-html */ +import { GlModal } from '@gitlab/ui'; import { __, s__, sprintf } from '~/locale'; import csrf from '~/lib/utils/csrf'; export default { components: { - DeprecatedModal, + GlModal, }, props: { actionUrl: { @@ -54,21 +55,38 @@ You are about to permanently delete %{yourAccount}, and all of the issues, merge Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), { yourAccount: `<strong>${s__('Profiles|your account')}</strong>`, - deleteAccount: `<strong>${s__('Profiles|Delete Account')}</strong>`, + deleteAccount: `<strong>${s__('Profiles|Delete account')}</strong>`, }, false, ); }, - }, - methods: { + primaryProps() { + return { + text: s__('Delete account'), + attributes: [ + { variant: 'danger', 'data-qa-selector': 'confirm_delete_account_button' }, + { category: 'primary' }, + { disabled: !this.canSubmit }, + ], + }; + }, + cancelProps() { + return { + text: s__('Cancel'), + }; + }, canSubmit() { if (this.confirmWithPassword) { return this.enteredPassword !== ''; } - return this.enteredUsername === this.username; }, + }, + methods: { onSubmit() { + if (!this.canSubmit) { + return; + } this.$refs.form.submit(); }, }, @@ -76,42 +94,39 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), </script> <template> - <deprecated-modal - id="delete-account-modal" - :title="s__('Profiles|Delete your account?')" - :text="text" - :primary-button-label="s__('Profiles|Delete account')" - :submit-disabled="!canSubmit()" - kind="danger" - @submit="onSubmit" + <gl-modal + modal-id="delete-account-modal" + title="Profiles" + :action-primary="primaryProps" + :action-cancel="cancelProps" + :ok-disabled="!canSubmit" + @primary="onSubmit" > - <template #body="props"> - <p v-html="props.text"></p> + <p v-html="text"></p> - <form ref="form" :action="actionUrl" method="post"> - <input type="hidden" name="_method" value="delete" /> - <input :value="csrfToken" type="hidden" name="authenticity_token" /> + <form ref="form" :action="actionUrl" method="post"> + <input type="hidden" name="_method" value="delete" /> + <input :value="csrfToken" type="hidden" name="authenticity_token" /> - <p id="input-label" v-html="inputLabel"></p> + <p id="input-label" v-html="inputLabel"></p> - <input - v-if="confirmWithPassword" - v-model="enteredPassword" - name="password" - class="form-control" - type="password" - data-qa-selector="password_confirmation_field" - aria-labelledby="input-label" - /> - <input - v-else - v-model="enteredUsername" - name="username" - class="form-control" - type="text" - aria-labelledby="input-label" - /> - </form> - </template> - </deprecated-modal> + <input + v-if="confirmWithPassword" + v-model="enteredPassword" + name="password" + class="form-control" + type="password" + data-qa-selector="password_confirmation_field" + aria-labelledby="input-label" + /> + <input + v-else + v-model="enteredUsername" + name="username" + class="form-control" + type="text" + aria-labelledby="input-label" + /> + </form> + </gl-modal> </template> diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue index 58025381cb2..4aaa2cff2ac 100644 --- a/app/assets/javascripts/profile/account/components/update_username.vue +++ b/app/assets/javascripts/profile/account/components/update_username.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { escape } from 'lodash'; import axios from '~/lib/utils/axios_utils'; import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue'; diff --git a/app/assets/javascripts/profile/account/index.js b/app/assets/javascripts/profile/account/index.js index f0d9642a2b2..5e89002b3bc 100644 --- a/app/assets/javascripts/profile/account/index.js +++ b/app/assets/javascripts/profile/account/index.js @@ -30,6 +30,9 @@ export default () => { }, mounted() { deleteAccountButton.classList.remove('disabled'); + deleteAccountButton.addEventListener('click', () => { + this.$root.$emit('bv::show::modal', 'delete-account-modal', '#delete-account-button'); + }); }, render(createElement) { return createElement('delete-account-modal', { diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 788553636f9..db2b0856e1b 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -110,7 +110,10 @@ const projectSelect = () => { }); }; -export default () => - import(/* webpackChunkName: 'select2' */ 'select2/select2') - .then(projectSelect) - .catch(() => {}); +export default () => { + if ($('.ajax-project-select').length) { + import(/* webpackChunkName: 'select2' */ 'select2/select2') + .then(projectSelect) + .catch(() => {}); + } +}; diff --git a/app/assets/javascripts/projects/commits/components/author_select.vue b/app/assets/javascripts/projects/commits/components/author_select.vue index a8589b50899..2204ec3cbe7 100644 --- a/app/assets/javascripts/projects/commits/components/author_select.vue +++ b/app/assets/javascripts/projects/commits/components/author_select.vue @@ -2,11 +2,11 @@ import { debounce } from 'lodash'; import { mapState, mapActions } from 'vuex'; import { - GlNewDropdown, - GlNewDropdownHeader, - GlNewDropdownItem, + GlDropdown, + GlDropdownSectionHeader, + GlDropdownItem, GlSearchBoxByType, - GlNewDropdownDivider, + GlDropdownDivider, GlTooltipDirective, } from '@gitlab/ui'; import { redirectTo } from '~/lib/utils/url_utility'; @@ -18,11 +18,11 @@ const tooltipMessage = __('Searching by both author and message is currently not export default { name: 'AuthorSelect', components: { - GlNewDropdown, - GlNewDropdownHeader, - GlNewDropdownItem, + GlDropdown, + GlDropdownSectionHeader, + GlDropdownItem, GlSearchBoxByType, - GlNewDropdownDivider, + GlDropdownDivider, }, directives: { GlTooltip: GlTooltipDirective, @@ -107,27 +107,27 @@ export default { <template> <div ref="dropdownContainer" v-gl-tooltip :title="tooltipTitle" :disabled="!hasSearchParam"> - <gl-new-dropdown + <gl-dropdown :text="dropdownText" :disabled="hasSearchParam" toggle-class="gl-py-3 gl-border-0" class="w-100 mt-2 mt-sm-0" > - <gl-new-dropdown-header> + <gl-dropdown-section-header> {{ __('Search by author') }} - </gl-new-dropdown-header> - <gl-new-dropdown-divider /> + </gl-dropdown-section-header> + <gl-dropdown-divider /> <gl-search-box-by-type v-model.trim="authorInput" - class="m-2" + class="gl-m-3" :placeholder="__('Search')" @input="searchAuthors" /> - <gl-new-dropdown-item :is-checked="!currentAuthor" @click="selectAuthor(null)"> + <gl-dropdown-item :is-checked="!currentAuthor" @click="selectAuthor(null)"> {{ __('Any Author') }} - </gl-new-dropdown-item> - <gl-new-dropdown-divider /> - <gl-new-dropdown-item + </gl-dropdown-item> + <gl-dropdown-divider /> + <gl-dropdown-item v-for="author in commitsAuthors" :key="author.id" :is-checked="author.name === currentAuthor" @@ -136,7 +136,7 @@ export default { @click="selectAuthor(author)" > {{ author.name }} - </gl-new-dropdown-item> - </gl-new-dropdown> + </gl-dropdown-item> + </gl-dropdown> </div> </template> diff --git a/app/assets/javascripts/projects/components/project_delete_button.vue b/app/assets/javascripts/projects/components/project_delete_button.vue index 4b27c5e3d30..2f3ff92d7ae 100644 --- a/app/assets/javascripts/projects/components/project_delete_button.vue +++ b/app/assets/javascripts/projects/components/project_delete_button.vue @@ -22,10 +22,10 @@ export default { strings: { alertTitle: __('You are about to permanently delete this project'), alertBody: __( - 'Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its respositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc.', + 'Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc.', ), modalBody: __( - "This action cannot be undone. You will lose the project's respository and all conent: issues, merge requests, etc.", + "This action cannot be undone. You will lose the project's repository and all content: issues, merge requests, etc.", ), }, }; diff --git a/app/assets/javascripts/projects/components/shared/delete_button.vue b/app/assets/javascripts/projects/components/shared/delete_button.vue index e3f4500d404..051bfcb732a 100644 --- a/app/assets/javascripts/projects/components/shared/delete_button.vue +++ b/app/assets/javascripts/projects/components/shared/delete_button.vue @@ -86,7 +86,7 @@ export default { <slot name="modal-body"></slot> <p class="gl-mb-1">{{ $options.strings.confirmText }}</p> <p> - <code>{{ confirmPhrase }}</code> + <code class="gl-white-space-pre-wrap">{{ confirmPhrase }}</code> </p> <gl-form-input id="confirm_name_input" 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 ee4a00dbc75..f404e6030f4 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,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { GlBreadcrumb, GlIcon } from '@gitlab/ui'; import WelcomePage from './welcome.vue'; import LegacyContainer from './legacy_container.vue'; 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 cd9a72996cf..022328cd8a2 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 @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { GlPopover } from '@gitlab/ui'; import Tracking from '~/tracking'; import LegacyContainer from './legacy_container.vue'; diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index ec0a83b5736..599aa52831b 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -1,13 +1,18 @@ import $ from 'jquery'; import DEFAULT_PROJECT_TEMPLATES from 'ee_else_ce/projects/default_project_templates'; import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils'; -import { convertToTitleCase, humanize, slugify } from '../lib/utils/text_utility'; +import { + convertToTitleCase, + humanize, + slugify, + convertUnicodeToAscii, +} from '../lib/utils/text_utility'; let hasUserDefinedProjectPath = false; let hasUserDefinedProjectName = false; const onProjectNameChange = ($projectNameInput, $projectPathInput) => { - const slug = slugify($projectNameInput.val()); + const slug = slugify(convertUnicodeToAscii($projectNameInput.val())); $projectPathInput.val(slug); }; diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js index 4dbf6675357..5d51b7ea57b 100644 --- a/app/assets/javascripts/projects/settings/access_dropdown.js +++ b/app/assets/javascripts/projects/settings/access_dropdown.js @@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils'; import { deprecatedCreateFlash as Flash } from '~/flash'; import { n__, s__, __ } from '~/locale'; import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVEL_NONE } from './constants'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default class AccessDropdown { constructor(options) { @@ -29,7 +30,7 @@ export default class AccessDropdown { initDropdown() { const { onSelect, onHide } = this.options; - this.$dropdown.glDropdown({ + initDeprecatedJQueryDropdown(this.$dropdown, { data: this.getData.bind(this), selectable: true, filterable: true, 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 d61569fcd6e..81367f7d6b4 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 @@ -118,7 +118,10 @@ export default { this.isTemplateSaving = true; this.service .updateTemplate({ selectedTemplate, outgoingName, projectKey }, this.isEnabled) - .then(() => this.showAlert(__('Template was successfully saved.'), 'success')) + .then(({ data }) => { + this.incomingEmail = data?.service_desk_address; + this.showAlert(__('Changes were successfully made.'), 'success'); + }) .catch(() => this.showAlert( __('An error occurred while saving the template. Please check if the template exists.'), 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 0b7433d6aaa..6a0810ad3a1 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 @@ -157,14 +157,16 @@ export default { }} </span> </template> - <gl-button - variant="success" - class="gl-mt-5" - :disabled="isTemplateSaving" - @click="onSaveTemplate" - > - {{ __('Save template') }} - </gl-button> + <div class="gl-display-flex gl-justify-content-end"> + <gl-button + variant="success" + class="gl-mt-5" + :disabled="isTemplateSaving" + @click="onSaveTemplate" + > + {{ __('Save changes') }} + </gl-button> + </div> </div> </div> </div> diff --git a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue b/app/assets/javascripts/prometheus_alerts/components/reset_key.vue index 05e769f5fc8..6f60141d7ab 100644 --- a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue +++ b/app/assets/javascripts/prometheus_alerts/components/reset_key.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { GlButton, GlFormGroup, GlFormInput, GlModal, GlModalDirective } from '@gitlab/ui'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import axios from '~/lib/utils/axios_utils'; diff --git a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js index def2f091947..202286a5fb4 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js +++ b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js @@ -1,4 +1,5 @@ import { __ } from '~/locale'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default class ProtectedTagAccessDropdown { constructor(options) { @@ -8,7 +9,7 @@ export default class ProtectedTagAccessDropdown { initDropdown() { const { onSelect } = this.options; - this.options.$dropdown.glDropdown({ + initDeprecatedJQueryDropdown(this.options.$dropdown, { data: this.options.data, selectable: true, inputId: this.options.$dropdown.data('inputId'), diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js index 03a5fe6b353..eb44f0c67fd 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_create.js +++ b/app/assets/javascripts/protected_tags/protected_tag_create.js @@ -23,7 +23,7 @@ export default class ProtectedTagCreate { }); // Select default - $allowedToCreateDropdown.data('glDropdown').selectRowAtIndex(0); + $allowedToCreateDropdown.data('deprecatedJQueryDropdown').selectRowAtIndex(0); // Protected tag dropdown this.createItemDropdown = new CreateItemDropdown({ diff --git a/app/assets/javascripts/ref/components/ref_results_section.vue b/app/assets/javascripts/ref/components/ref_results_section.vue index c8f5c66b0c1..dc74f86fd70 100644 --- a/app/assets/javascripts/ref/components/ref_results_section.vue +++ b/app/assets/javascripts/ref/components/ref_results_section.vue @@ -1,12 +1,12 @@ <script> -import { GlNewDropdownHeader, GlNewDropdownItem, GlBadge, GlIcon } from '@gitlab/ui'; +import { GlDropdownSectionHeader, GlDropdownItem, GlBadge, GlIcon } from '@gitlab/ui'; import { s__ } from '~/locale'; export default { name: 'RefResultsSection', components: { - GlNewDropdownHeader, - GlNewDropdownItem, + GlDropdownSectionHeader, + GlDropdownItem, GlBadge, GlIcon, }, @@ -84,12 +84,12 @@ export default { <template> <div> - <gl-new-dropdown-header> + <gl-dropdown-section-header> <div class="gl-display-flex align-items-center" data-testid="section-header"> <span class="gl-mr-2 gl-mb-1">{{ sectionTitle }}</span> <gl-badge variant="neutral">{{ totalCountText }}</gl-badge> </div> - </gl-new-dropdown-header> + </gl-dropdown-section-header> <template v-if="error"> <div class="gl-display-flex align-items-start text-danger gl-ml-4 gl-mr-4 gl-mb-3"> <gl-icon name="error" class="gl-mr-2 gl-mt-2 gl-flex-shrink-0" /> @@ -97,7 +97,7 @@ export default { </div> </template> <template v-else> - <gl-new-dropdown-item + <gl-dropdown-item v-for="item in items" :key="item.name" @click="$emit('selected', item.value || item.name)" @@ -118,7 +118,7 @@ export default { s__('DefaultBranchLabel|default') }}</gl-badge> </div> - </gl-new-dropdown-item> + </gl-dropdown-item> </template> </div> </template> diff --git a/app/assets/javascripts/ref/components/ref_selector.vue b/app/assets/javascripts/ref/components/ref_selector.vue index e388604ed92..85b123530b5 100644 --- a/app/assets/javascripts/ref/components/ref_selector.vue +++ b/app/assets/javascripts/ref/components/ref_selector.vue @@ -1,9 +1,9 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; import { - GlNewDropdown, - GlNewDropdownDivider, - GlNewDropdownHeader, + GlDropdown, + GlDropdownDivider, + GlDropdownSectionHeader, GlSearchBoxByType, GlSprintf, GlIcon, @@ -18,9 +18,9 @@ export default { name: 'RefSelector', store: createStore(), components: { - GlNewDropdown, - GlNewDropdownDivider, - GlNewDropdownHeader, + GlDropdown, + GlDropdownDivider, + GlDropdownSectionHeader, GlSearchBoxByType, GlSprintf, GlIcon, @@ -87,6 +87,15 @@ export default { }, }, created() { + // This method is defined here instead of in `methods` + // because we need to access the .cancel() method + // lodash attaches to the function, which is + // made inaccessible by Vue. More info: + // https://stackoverflow.com/a/52988020/1063392 + this.debouncedSearch = debounce(function search() { + this.search(this.query); + }, SEARCH_DEBOUNCE_MS); + this.setProjectId(this.projectId); this.search(this.query); }, @@ -95,9 +104,13 @@ export default { focusSearchBox() { this.$refs.searchBox.$el.querySelector('input').focus(); }, - onSearchBoxInput: debounce(function search() { + onSearchBoxEnter() { + this.debouncedSearch.cancel(); this.search(this.query); - }, SEARCH_DEBOUNCE_MS), + }, + onSearchBoxInput() { + this.debouncedSearch(); + }, selectRef(ref) { this.setSelectedRef(ref); this.$emit('input', this.selectedRef); @@ -107,7 +120,7 @@ export default { </script> <template> - <gl-new-dropdown v-bind="$attrs" class="ref-selector" @shown="focusSearchBox"> + <gl-dropdown v-bind="$attrs" class="ref-selector" @shown="focusSearchBox"> <template slot="button-content"> <span class="gl-flex-grow-1 gl-ml-2 gl-text-gray-400" data-testid="button-content"> <span v-if="selectedRef" class="gl-font-monospace">{{ selectedRef }}</span> @@ -117,11 +130,11 @@ export default { </template> <div class="gl-display-flex gl-flex-direction-column ref-selector-dropdown-content"> - <gl-new-dropdown-header> + <gl-dropdown-section-header> <span class="gl-text-center gl-display-block">{{ i18n.dropdownHeader }}</span> - </gl-new-dropdown-header> + </gl-dropdown-section-header> - <gl-new-dropdown-divider /> + <gl-dropdown-divider /> <gl-search-box-by-type ref="searchBox" @@ -129,6 +142,7 @@ export default { class="gl-m-3" :placeholder="i18n.searchPlaceholder" @input="onSearchBoxInput" + @keydown.enter.prevent="onSearchBoxEnter" /> <div class="gl-flex-grow-1 gl-overflow-y-auto"> @@ -161,7 +175,7 @@ export default { @selected="selectRef($event)" /> - <gl-new-dropdown-divider v-if="showTagsSection || showCommitsSection" /> + <gl-dropdown-divider v-if="showTagsSection || showCommitsSection" /> </template> <template v-if="showTagsSection"> @@ -176,7 +190,7 @@ export default { @selected="selectRef($event)" /> - <gl-new-dropdown-divider v-if="showCommitsSection" /> + <gl-dropdown-divider v-if="showCommitsSection" /> </template> <template v-if="showCommitsSection"> @@ -194,5 +208,5 @@ export default { </template> </div> </div> - </gl-new-dropdown> + </gl-dropdown> </template> diff --git a/app/assets/javascripts/ref_select_dropdown.js b/app/assets/javascripts/ref_select_dropdown.js index 2e0113271df..af8729c1d08 100644 --- a/app/assets/javascripts/ref_select_dropdown.js +++ b/app/assets/javascripts/ref_select_dropdown.js @@ -1,11 +1,11 @@ import $ from 'jquery'; -import '~/gl_dropdown'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; class RefSelectDropdown { constructor($dropdownButton, availableRefs) { const availableRefsValue = availableRefs || JSON.parse(document.getElementById('availableRefs').innerHTML); - $dropdownButton.glDropdown({ + initDeprecatedJQueryDropdown($dropdownButton, { data: availableRefsValue, filterable: true, filterByText: true, diff --git a/app/assets/javascripts/registry/explorer/components/delete_button.vue b/app/assets/javascripts/registry/explorer/components/delete_button.vue index dab6a26ea16..ee856a3e546 100644 --- a/app/assets/javascripts/registry/explorer/components/delete_button.vue +++ b/app/assets/javascripts/registry/explorer/components/delete_button.vue @@ -47,7 +47,6 @@ export default { :disabled="disabled" :title="title" :aria-label="title" - category="secondary" variant="danger" icon="remove" @click="$emit('delete')" 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 c254dd05aa4..ff613daf7fa 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,9 +1,10 @@ <script> import { GlSprintf } from '@gitlab/ui'; +import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import { DETAILS_PAGE_TITLE } from '../../constants/index'; export default { - components: { GlSprintf }, + components: { GlSprintf, TitleArea }, props: { imageName: { type: String, @@ -18,13 +19,13 @@ export default { </script> <template> - <div class="gl-display-flex gl-my-2 gl-align-items-center"> - <h4> + <title-area> + <template #title> <gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE"> <template #imageName> {{ imageName }} </template> </gl-sprintf> - </h4> - </div> + </template> + </title-area> </template> 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 8494967ab57..328026d0953 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 @@ -67,7 +67,6 @@ export default { :key="tag.path" :tag="tag" :first="index === 0" - :last="index === tags.length - 1" :selected="selectedItems[tag.name]" :is-desktop="isDesktop" @select="updateSelectedItems(tag.name)" 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 8ec5cebbe8e..661213733ac 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 @@ -5,9 +5,9 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import { formatDate } from '~/lib/utils/datetime_utility'; +import ListItem from '~/vue_shared/components/registry/list_item.vue'; import DeleteButton from '../delete_button.vue'; -import ListItem from '../list_item.vue'; -import DetailsRow from '~/registry/shared/components/details_row.vue'; +import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; import { REMOVE_TAG_BUTTON_TITLE, DIGEST_LABEL, 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 36a46ed58f4..85d87dab042 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 @@ -1,8 +1,8 @@ <script> -import { GlDeprecatedDropdown, GlFormGroup, GlFormInputGroup } from '@gitlab/ui'; +import { GlDeprecatedDropdown } from '@gitlab/ui'; import { mapGetters } from 'vuex'; import Tracking from '~/tracking'; -import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; import { QUICK_START, LOGIN_COMMAND_LABEL, @@ -13,22 +13,23 @@ import { COPY_PUSH_TITLE, } from '../../constants/index'; +const trackingLabel = 'quickstart_dropdown'; + export default { components: { GlDeprecatedDropdown, - GlFormGroup, - GlFormInputGroup, - ClipboardButton, + CodeInstruction, }, - mixins: [Tracking.mixin({ label: 'quickstart_dropdown' })], + mixins: [Tracking.mixin({ label: trackingLabel })], + trackingLabel, i18n: { - dropdownTitle: QUICK_START, - loginCommandLabel: LOGIN_COMMAND_LABEL, - copyLoginTitle: COPY_LOGIN_TITLE, - buildCommandLabel: BUILD_COMMAND_LABEL, - copyBuildTitle: COPY_BUILD_TITLE, - pushCommandLabel: PUSH_COMMAND_LABEL, - copyPushTitle: COPY_PUSH_TITLE, + QUICK_START, + LOGIN_COMMAND_LABEL, + COPY_LOGIN_TITLE, + BUILD_COMMAND_LABEL, + COPY_BUILD_TITLE, + PUSH_COMMAND_LABEL, + COPY_PUSH_TITLE, }, computed: { ...mapGetters(['dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand']), @@ -37,7 +38,7 @@ export default { </script> <template> <gl-deprecated-dropdown - :text="$options.i18n.dropdownTitle" + :text="$options.i18n.QUICK_START" variant="primary" size="sm" right @@ -45,59 +46,30 @@ export default { > <!-- This li is used as a container since gl-dropdown produces a root ul, this mimics the functionality exposed by b-dropdown-form --> <li role="presentation" class="px-2 py-1 dropdown-menu-large"> - <form> - <gl-form-group - label-size="sm" - label-for="docker-login-btn" - :label="$options.i18n.loginCommandLabel" - > - <gl-form-input-group id="docker-login-btn" :value="dockerLoginCommand" readonly> - <template #append> - <clipboard-button - class="border" - :text="dockerLoginCommand" - :title="$options.i18n.copyLoginTitle" - @click.native="track('click_copy_login')" - /> - </template> - </gl-form-input-group> - </gl-form-group> + <code-instruction + :label="$options.i18n.LOGIN_COMMAND_LABEL" + :instruction="dockerLoginCommand" + :copy-text="$options.i18n.COPY_LOGIN_TITLE" + tracking-action="click_copy_login" + :tracking-label="$options.trackingLabel" + /> - <gl-form-group - label-size="sm" - label-for="docker-build-btn" - :label="$options.i18n.buildCommandLabel" - > - <gl-form-input-group id="docker-build-btn" :value="dockerBuildCommand" readonly> - <template #append> - <clipboard-button - class="border" - :text="dockerBuildCommand" - :title="$options.i18n.copyBuildTitle" - @click.native="track('click_copy_build')" - /> - </template> - </gl-form-input-group> - </gl-form-group> + <code-instruction + :label="$options.i18n.BUILD_COMMAND_LABEL" + :instruction="dockerBuildCommand" + :copy-text="$options.i18n.COPY_BUILD_TITLE" + tracking-action="click_copy_build" + :tracking-label="$options.trackingLabel" + /> - <gl-form-group - class="mb-0" - label-size="sm" - label-for="docker-push-btn" - :label="$options.i18n.pushCommandLabel" - > - <gl-form-input-group id="docker-push-btn" :value="dockerPushCommand" readonly> - <template #append> - <clipboard-button - class="border" - :text="dockerPushCommand" - :title="$options.i18n.copyPushTitle" - @click.native="track('click_copy_push')" - /> - </template> - </gl-form-input-group> - </gl-form-group> - </form> + <code-instruction + class="mb-0" + :label="$options.i18n.PUSH_COMMAND_LABEL" + :instruction="dockerPushCommand" + :copy-text="$options.i18n.COPY_PUSH_TITLE" + tracking-action="click_copy_push" + :tracking-label="$options.trackingLabel" + /> </li> </gl-deprecated-dropdown> </template> 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 65cf51fd1d1..d1b9894da0e 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 @@ -38,7 +38,6 @@ export default { :key="index" :item="listItem" :first="index === 0" - :last="index === images.length - 1" @delete="$emit('delete', $event)" /> 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 102311c6062..32bf27f1143 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 @@ -2,7 +2,7 @@ import { GlTooltipDirective, GlIcon, GlSprintf } from '@gitlab/ui'; import { n__ } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import ListItem from '../list_item.vue'; +import ListItem from '~/vue_shared/components/registry/list_item.vue'; import DeleteButton from '../delete_button.vue'; import { 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 c7b4fd5f4b4..7be68e77def 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 @@ -1,6 +1,8 @@ <script> -import { GlSprintf, GlLink, GlIcon } from '@gitlab/ui'; -import { n__ } from '~/locale'; +import { GlSprintf, GlLink } from '@gitlab/ui'; +import TitleArea from '~/vue_shared/components/registry/title_area.vue'; +import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; +import { n__, sprintf } from '~/locale'; import { approximateDuration, calculateRemainingMilliseconds } from '~/lib/utils/datetime_utility'; import { @@ -13,9 +15,10 @@ import { export default { components: { - GlIcon, GlSprintf, GlLink, + TitleArea, + MetadataItem, }, props: { expirationPolicy: { @@ -56,11 +59,12 @@ export default { }, computed: { imagesCountText() { - return n__( + const pluralisedString = n__( 'ContainerRegistry|%{count} Image repository', 'ContainerRegistry|%{count} Image repositories', this.imagesCount, ); + return sprintf(pluralisedString, { count: this.imagesCount }); }, timeTillRun() { const difference = calculateRemainingMilliseconds(this.expirationPolicy?.next_run_at); @@ -71,7 +75,7 @@ export default { }, expirationPolicyText() { return this.expirationPolicyEnabled - ? EXPIRATION_POLICY_WILL_RUN_IN + ? sprintf(EXPIRATION_POLICY_WILL_RUN_IN, { time: this.timeTillRun }) : EXPIRATION_POLICY_DISABLED_TEXT; }, showExpirationPolicyTip() { @@ -85,37 +89,29 @@ export default { <template> <div> - <div - class="gl-display-flex gl-justify-content-space-between gl-align-items-center" - data-testid="header" - > - <h4 data-testid="title">{{ $options.i18n.CONTAINER_REGISTRY_TITLE }}</h4> - <div class="gl-display-none d-sm-block" data-testid="commands-slot"> + <title-area :title="$options.i18n.CONTAINER_REGISTRY_TITLE"> + <template #right-actions> <slot name="commands"></slot> - </div> - </div> - <div - v-if="imagesCount" - class="gl-display-flex gl-align-items-center gl-mt-1 gl-mb-3 gl-text-gray-500" - data-testid="subheader" - > - <span class="gl-mr-3" data-testid="images-count"> - <gl-icon class="gl-mr-1" name="container-image" /> - <gl-sprintf :message="imagesCountText"> - <template #count> - {{ imagesCount }} - </template> - </gl-sprintf> - </span> - <span v-if="!hideExpirationPolicyData" data-testid="expiration-policy"> - <gl-icon class="gl-mr-1" name="expire" /> - <gl-sprintf :message="expirationPolicyText"> - <template #time> - {{ timeTillRun }} - </template> - </gl-sprintf> - </span> - </div> + </template> + <template #metadata_count> + <metadata-item + v-if="imagesCount" + data-testid="images-count" + icon="container-image" + :text="imagesCountText" + /> + </template> + <template #metadata_exp_policies> + <metadata-item + v-if="!hideExpirationPolicyData" + data-testid="expiration-policy" + icon="expire" + :text="expirationPolicyText" + size="xl" + /> + </template> + </title-area> + <div data-testid="info-area"> <p> <span data-testid="default-intro"> diff --git a/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue b/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue index d935ca091a1..146d1434b18 100644 --- a/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue +++ b/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue @@ -1,7 +1,9 @@ <script> import { initial, first, last } from 'lodash'; +import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; export default { + directives: { SafeHtml }, props: { crumbs: { type: Array, @@ -41,14 +43,14 @@ export default { <li v-for="(crumb, index) in rootCrumbs" :key="index" + v-safe-html="crumb.innerHTML" :class="crumb.className" - v-html="crumb.innerHTML" ></li> <li v-if="!isRootRoute"> <router-link ref="rootRouteLink" :to="rootRoute.path"> {{ rootRoute.meta.nameGenerator(rootRoute) }} </router-link> - <component :is="divider.tagName" :class="divider.classList" v-html="divider.innerHTML" /> + <component :is="divider.tagName" v-safe-html="divider.innerHTML" :class="divider.classList" /> </li> <li> <component :is="lastCrumb.tagName" ref="lastCrumb" :class="lastCrumb.className"> diff --git a/app/assets/javascripts/registry/explorer/stores/index.js b/app/assets/javascripts/registry/explorer/stores/index.js index 54a8e0e1c1c..18e3351ed13 100644 --- a/app/assets/javascripts/registry/explorer/stores/index.js +++ b/app/assets/javascripts/registry/explorer/stores/index.js @@ -7,7 +7,6 @@ import state from './state'; Vue.use(Vuex); -// eslint-disable-next-line import/prefer-default-export export const createStore = () => new Vuex.Store({ state, diff --git a/app/assets/javascripts/registry/explorer/utils.js b/app/assets/javascripts/registry/explorer/utils.js index b1df87c6993..44262a6cbb6 100644 --- a/app/assets/javascripts/registry/explorer/utils.js +++ b/app/assets/javascripts/registry/explorer/utils.js @@ -1,2 +1 @@ -// eslint-disable-next-line import/prefer-default-export export const decodeAndParse = param => JSON.parse(window.atob(param)); diff --git a/app/assets/javascripts/registry/settings/components/settings_form.vue b/app/assets/javascripts/registry/settings/components/settings_form.vue index f129922c1d2..7a26fb5cbee 100644 --- a/app/assets/javascripts/registry/settings/components/settings_form.vue +++ b/app/assets/javascripts/registry/settings/components/settings_form.vue @@ -1,7 +1,7 @@ <script> import { get } from 'lodash'; import { mapActions, mapState, mapGetters } from 'vuex'; -import { GlCard, GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui'; +import { GlCard, GlButton, GlLoadingIcon } from '@gitlab/ui'; import Tracking from '~/tracking'; import { mapComputed } from '~/vuex_shared/bindings'; import { @@ -14,7 +14,7 @@ import { SET_CLEANUP_POLICY_BUTTON, CLEANUP_POLICY_CARD_HEADER } from '../consta export default { components: { GlCard, - GlDeprecatedButton, + GlButton, GlLoadingIcon, ExpirationPolicyFields, }, @@ -104,24 +104,25 @@ export default { </template> <template #footer> <div class="gl-display-flex gl-justify-content-end"> - <gl-deprecated-button + <gl-button ref="cancel-button" type="reset" class="gl-mr-3 gl-display-block" :disabled="isCancelButtonDisabled" > {{ __('Cancel') }} - </gl-deprecated-button> - <gl-deprecated-button + </gl-button> + <gl-button ref="save-button" type="submit" :disabled="isSubmitButtonDisabled" variant="success" + category="primary" class="gl-display-flex gl-justify-content-center gl-align-items-center js-no-auto-disable" > {{ $options.i18n.SET_CLEANUP_POLICY_BUTTON }} <gl-loading-icon v-if="isLoading" class="gl-ml-3" /> - </gl-deprecated-button> + </gl-button> </div> </template> </gl-card> diff --git a/app/assets/javascripts/related_issues/components/add_issuable_form.vue b/app/assets/javascripts/related_issues/components/add_issuable_form.vue new file mode 100644 index 00000000000..63d61989cba --- /dev/null +++ b/app/assets/javascripts/related_issues/components/add_issuable_form.vue @@ -0,0 +1,207 @@ +<script> +import { GlFormGroup, GlFormRadioGroup, GlButton } from '@gitlab/ui'; +import { __ } from '~/locale'; +import RelatedIssuableInput from './related_issuable_input.vue'; +import { mergeUrlParams } from '~/lib/utils/url_utility'; + +import { + issuableTypesMap, + itemAddFailureTypesMap, + linkedIssueTypesMap, + addRelatedIssueErrorMap, + addRelatedItemErrorMap, +} from '../constants'; + +export default { + name: 'AddIssuableForm', + components: { + GlFormGroup, + GlFormRadioGroup, + RelatedIssuableInput, + GlButton, + }, + props: { + inputValue: { + type: String, + required: true, + }, + pendingReferences: { + type: Array, + required: false, + default: () => [], + }, + autoCompleteSources: { + type: Object, + required: false, + default: () => ({}), + }, + showCategorizedIssues: { + type: Boolean, + required: false, + default: false, + }, + isSubmitting: { + type: Boolean, + required: false, + default: false, + }, + pathIdSeparator: { + type: String, + required: true, + }, + issuableType: { + type: String, + required: false, + default: issuableTypesMap.ISSUE, + }, + hasError: { + type: Boolean, + required: false, + default: false, + }, + itemAddFailureType: { + type: String, + required: false, + default: itemAddFailureTypesMap.NOT_FOUND, + }, + itemAddFailureMessage: { + type: String, + required: false, + default: '', + }, + confidential: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + linkedIssueType: linkedIssueTypesMap.RELATES_TO, + linkedIssueTypes: [ + { + text: __('relates to'), + value: linkedIssueTypesMap.RELATES_TO, + }, + { + text: __('blocks'), + value: linkedIssueTypesMap.BLOCKS, + }, + { + text: __('is blocked by'), + value: linkedIssueTypesMap.IS_BLOCKED_BY, + }, + ], + }; + }, + computed: { + isSubmitButtonDisabled() { + return ( + (this.inputValue.length === 0 && this.pendingReferences.length === 0) || this.isSubmitting + ); + }, + addRelatedErrorMessage() { + if (this.itemAddFailureMessage) { + return this.itemAddFailureMessage; + } else if (this.itemAddFailureType === itemAddFailureTypesMap.NOT_FOUND) { + return addRelatedIssueErrorMap[this.issuableType]; + } + // Only other failure is MAX_NUMBER_OF_CHILD_EPICS at the moment + return addRelatedItemErrorMap[this.itemAddFailureType]; + }, + transformedAutocompleteSources() { + if (!this.confidential) { + return this.autoCompleteSources; + } + + if (!this.autoCompleteSources?.issues || !this.autoCompleteSources?.epics) { + return this.autoCompleteSources; + } + + return { + ...this.autoCompleteSources, + issues: mergeUrlParams({ confidential_only: true }, this.autoCompleteSources.issues), + epics: mergeUrlParams({ confidential_only: true }, this.autoCompleteSources.epics), + }; + }, + }, + methods: { + onPendingIssuableRemoveRequest(params) { + this.$emit('pendingIssuableRemoveRequest', params); + }, + onFormSubmit() { + this.$emit('addIssuableFormSubmit', { + pendingReferences: this.$refs.relatedIssuableInput.$refs.input.value, + linkedIssueType: this.linkedIssueType, + }); + }, + onFormCancel() { + this.$emit('addIssuableFormCancel'); + }, + onAddIssuableFormInput(params) { + this.$emit('addIssuableFormInput', params); + }, + onAddIssuableFormBlur(params) { + this.$emit('addIssuableFormBlur', params); + }, + }, +}; +</script> + +<template> + <form @submit.prevent="onFormSubmit"> + <template v-if="showCategorizedIssues"> + <gl-form-group + :label="__('The current issue')" + label-for="linked-issue-type-radio" + label-class="label-bold" + class="mb-2" + > + <gl-form-radio-group + id="linked-issue-type-radio" + v-model="linkedIssueType" + :options="linkedIssueTypes" + :checked="linkedIssueType" + /> + </gl-form-group> + <p class="bold"> + {{ __('the following issue(s)') }} + </p> + </template> + <related-issuable-input + ref="relatedIssuableInput" + input-id="add-related-issues-form-input" + :confidential="confidential" + :focus-on-mount="true" + :references="pendingReferences" + :path-id-separator="pathIdSeparator" + :input-value="inputValue" + :auto-complete-sources="transformedAutocompleteSources" + :auto-complete-options="{ issues: true, epics: true }" + :issuable-type="issuableType" + @pendingIssuableRemoveRequest="onPendingIssuableRemoveRequest" + @formCancel="onFormCancel" + @addIssuableFormBlur="onAddIssuableFormBlur" + @addIssuableFormInput="onAddIssuableFormInput" + /> + <p v-if="hasError" class="gl-field-error"> + {{ addRelatedErrorMessage }} + </p> + <div class="add-issuable-form-actions clearfix"> + <gl-button + ref="addButton" + category="primary" + variant="success" + :disabled="isSubmitButtonDisabled" + :loading="isSubmitting" + type="submit" + class="js-add-issuable-form-add-button float-left qa-add-issue-button" + > + {{ __('Add') }} + </gl-button> + <gl-button class="float-right" @click="onFormCancel"> + {{ __('Cancel') }} + </gl-button> + </div> + </form> +</template> diff --git a/app/assets/javascripts/related_issues/components/issue_token.vue b/app/assets/javascripts/related_issues/components/issue_token.vue new file mode 100644 index 00000000000..31d0c7dbbb0 --- /dev/null +++ b/app/assets/javascripts/related_issues/components/issue_token.vue @@ -0,0 +1,115 @@ +<script> +import { GlIcon } from '@gitlab/ui'; +import { __, sprintf } from '~/locale'; +import relatedIssuableMixin from '~/vue_shared/mixins/related_issuable_mixin'; + +export default { + name: 'IssueToken', + components: { + GlIcon, + }, + mixins: [relatedIssuableMixin], + props: { + isCondensed: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + removeButtonLabel() { + const { displayReference } = this; + /* + * Giving false as third argument to prevent unescaping of ampersand in + * epic reference. Eg. &42 will remain &42 instead of &42 + * + * https://docs.gitlab.com/ee/development/i18n/externalization.html#interpolation + */ + return sprintf(__('Remove %{displayReference}'), { displayReference }, false); + }, + stateTitle() { + if (this.isCondensed) return ''; + + return this.isOpen ? __('Open') : __('Closed'); + }, + innerComponentType() { + return this.isCondensed ? 'span' : 'div'; + }, + issueTitle() { + return this.isCondensed ? this.title : ''; + }, + }, +}; +</script> + +<template> + <div + :class="{ + 'issue-token': isCondensed, + 'flex-row issuable-info-container': !isCondensed, + }" + > + <component + :is="computedLinkElementType" + ref="link" + v-tooltip + :class="{ + 'issue-token-link': isCondensed, + 'issuable-main-info': !isCondensed, + }" + :href="computedPath" + :title="issueTitle" + data-placement="top" + > + <component + :is="innerComponentType" + v-if="hasTitle" + ref="title" + :class="{ + 'issue-token-title issue-token-end': isCondensed, + 'issue-title block-truncated': !isCondensed, + 'issue-token-title-standalone': !canRemove, + }" + class="js-issue-token-title" + > + <span class="issue-token-title-text">{{ title }}</span> + </component> + <component + :is="innerComponentType" + ref="reference" + :class="{ + 'issue-token-reference': isCondensed, + 'issuable-info': !isCondensed, + }" + > + <gl-icon + v-if="hasState" + v-tooltip + :class="iconClass" + :name="iconName" + :size="12" + :title="stateTitle" + :aria-label="state" + /> + {{ displayReference }} + </component> + </component> + <button + v-if="canRemove" + ref="removeButton" + v-tooltip + :class="{ + 'issue-token-remove-button': isCondensed, + 'btn btn-default': !isCondensed, + }" + :title="removeButtonLabel" + :aria-label="removeButtonLabel" + :disabled="removeDisabled" + type="button" + class="js-issue-token-remove-button" + @click="onRemoveRequest" + > + <gl-icon name="close" aria-hidden="true" /> + </button> + </div> +</template> diff --git a/app/assets/javascripts/related_issues/components/related_issuable_input.vue b/app/assets/javascripts/related_issues/components/related_issuable_input.vue new file mode 100644 index 00000000000..1931cfb2c00 --- /dev/null +++ b/app/assets/javascripts/related_issues/components/related_issuable_input.vue @@ -0,0 +1,231 @@ +<script> +import $ from 'jquery'; +import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete'; +import issueToken from './issue_token.vue'; +import { + autoCompleteTextMap, + inputPlaceholderConfidentialTextMap, + inputPlaceholderTextMap, + issuableTypesMap, +} from '../constants'; + +const SPACE_FACTOR = 1; + +export default { + name: 'RelatedIssuableInput', + components: { + issueToken, + }, + props: { + inputId: { + type: String, + required: false, + default: '', + }, + references: { + type: Array, + required: false, + default: () => [], + }, + pathIdSeparator: { + type: String, + required: true, + }, + inputValue: { + type: String, + required: false, + default: '', + }, + focusOnMount: { + type: Boolean, + required: false, + default: false, + }, + autoCompleteSources: { + type: Object, + required: false, + default: () => ({}), + }, + autoCompleteOptions: { + type: Object, + required: false, + default: () => ({}), + }, + issuableType: { + type: String, + required: false, + default: issuableTypesMap.ISSUE, + }, + confidential: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + isInputFocused: false, + isAutoCompleteOpen: false, + areEventsAssigned: false, + }; + }, + computed: { + inputPlaceholder() { + const { issuableType, allowAutoComplete, confidential } = this; + const inputPlaceholderMapping = confidential + ? inputPlaceholderConfidentialTextMap + : inputPlaceholderTextMap; + const allowAutoCompleteText = autoCompleteTextMap[allowAutoComplete][issuableType]; + return `${inputPlaceholderMapping[issuableType]}${allowAutoCompleteText}`; + }, + allowAutoComplete() { + return Object.keys(this.autoCompleteSources).length > 0; + }, + }, + mounted() { + this.setupAutoComplete(); + if (this.focusOnMount) { + this.$nextTick() + .then(() => { + this.$refs.input.focus(); + }) + .catch(() => {}); + } + }, + beforeUpdate() { + this.setupAutoComplete(); + }, + beforeDestroy() { + const $input = $(this.$refs.input); + $input.off('shown-issues.atwho'); + $input.off('hidden-issues.atwho'); + $input.off('inserted-issues.atwho', this.onInput); + }, + methods: { + onAutoCompleteToggled(isOpen) { + this.isAutoCompleteOpen = isOpen; + }, + onInputWrapperClick() { + this.$refs.input.focus(); + }, + onInput() { + const { value } = this.$refs.input; + const caretPos = this.$refs.input.selectionStart; + const rawRefs = value.split(/\s/); + let touchedReference; + let position = 0; + + const untouchedRawRefs = rawRefs + .filter(ref => { + let isTouched = false; + + if (caretPos >= position && caretPos <= position + ref.length) { + touchedReference = ref; + isTouched = true; + } + + position = position + ref.length + SPACE_FACTOR; + + return !isTouched; + }) + .filter(ref => ref.trim().length > 0); + + this.$emit('addIssuableFormInput', { + newValue: value, + untouchedRawReferences: untouchedRawRefs, + touchedReference, + caretPos, + }); + }, + onBlur(event) { + // Early exit if this Blur event is caused by card header + const container = this.$root.$el.querySelector('.js-button-container'); + if (container && container.contains(event.relatedTarget)) { + return; + } + + this.isInputFocused = false; + + // Avoid tokenizing partial input when clicking an autocomplete item + if (!this.isAutoCompleteOpen) { + const { value } = this.$refs.input; + // Avoid event emission when only pathIdSeparator has been typed + if (value !== this.pathIdSeparator) { + this.$emit('addIssuableFormBlur', value); + } + } + }, + onFocus() { + this.isInputFocused = true; + }, + setupAutoComplete() { + const $input = $(this.$refs.input); + + if (this.allowAutoComplete) { + this.gfmAutoComplete = new GfmAutoComplete(this.autoCompleteSources); + this.gfmAutoComplete.setup($input, this.autoCompleteOptions); + } + + if (!this.areEventsAssigned) { + $input.on('shown-issues.atwho', this.onAutoCompleteToggled.bind(this, true)); + $input.on('hidden-issues.atwho', this.onAutoCompleteToggled.bind(this, true)); + } + this.areEventsAssigned = true; + }, + onIssuableFormWrapperClick() { + this.$refs.input.focus(); + }, + }, +}; +</script> + +<template> + <div + ref="issuableFormWrapper" + :class="{ focus: isInputFocused }" + class="add-issuable-form-input-wrapper form-control gl-field-error-outline" + role="button" + @click="onIssuableFormWrapperClick" + > + <ul class="add-issuable-form-input-token-list"> + <!-- + We need to ensure this key changes any time the pendingReferences array is updated + else two consecutive pending ref strings in an array with the same name will collide + and cause odd behavior when one is removed. + --> + <li + v-for="(reference, index) in references" + :key="`related-issues-token-${reference}`" + class="js-add-issuable-form-token-list-item add-issuable-form-token-list-item" + > + <issue-token + :id-key="index" + :display-reference="reference.text || reference" + :can-remove="true" + :is-condensed="true" + :path-id-separator="pathIdSeparator" + event-namespace="pendingIssuable" + @pendingIssuableRemoveRequest=" + params => { + $emit('pendingIssuableRemoveRequest', params); + } + " + /> + </li> + <li class="add-issuable-form-input-list-item"> + <input + :id="inputId" + ref="input" + :value="inputValue" + :placeholder="inputPlaceholder" + type="text" + class="js-add-issuable-form-input add-issuable-form-input qa-add-issue-input" + @input="onInput" + @focus="onFocus" + @blur="onBlur" + @keyup.escape.exact="$emit('addIssuableFormCancel')" + /> + </li> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/related_issues/components/related_issues_block.vue b/app/assets/javascripts/related_issues/components/related_issues_block.vue new file mode 100644 index 00000000000..f7a79c62716 --- /dev/null +++ b/app/assets/javascripts/related_issues/components/related_issues_block.vue @@ -0,0 +1,215 @@ +<script> +import { GlLink, GlIcon, GlButton } from '@gitlab/ui'; +import AddIssuableForm from './add_issuable_form.vue'; +import RelatedIssuesList from './related_issues_list.vue'; +import { + issuableIconMap, + issuableQaClassMap, + linkedIssueTypesMap, + linkedIssueTypesTextMap, +} from '../constants'; + +export default { + name: 'RelatedIssuesBlock', + components: { + GlLink, + GlButton, + GlIcon, + AddIssuableForm, + RelatedIssuesList, + }, + props: { + isFetching: { + type: Boolean, + required: false, + default: false, + }, + isSubmitting: { + type: Boolean, + required: false, + default: false, + }, + relatedIssues: { + type: Array, + required: false, + default: () => [], + }, + canAdmin: { + type: Boolean, + required: false, + default: false, + }, + canReorder: { + type: Boolean, + required: false, + default: false, + }, + isFormVisible: { + type: Boolean, + required: false, + default: false, + }, + pendingReferences: { + type: Array, + required: false, + default: () => [], + }, + inputValue: { + type: String, + required: false, + default: '', + }, + pathIdSeparator: { + type: String, + required: true, + }, + helpPath: { + type: String, + required: false, + default: '', + }, + autoCompleteSources: { + type: Object, + required: false, + default: () => ({}), + }, + issuableType: { + type: String, + required: true, + }, + showCategorizedIssues: { + type: Boolean, + required: false, + default: true, + }, + }, + computed: { + hasRelatedIssues() { + return this.relatedIssues.length > 0; + }, + categorisedIssues() { + if (this.showCategorizedIssues) { + return Object.values(linkedIssueTypesMap) + .map(linkType => ({ + linkType, + issues: this.relatedIssues.filter(issue => issue.linkType === linkType), + })) + .filter(obj => obj.issues.length > 0); + } + + return [{ issues: this.relatedIssues }]; + }, + shouldShowTokenBody() { + return this.hasRelatedIssues || this.isFetching; + }, + hasBody() { + return this.isFormVisible || this.shouldShowTokenBody; + }, + badgeLabel() { + return this.isFetching && this.relatedIssues.length === 0 ? '...' : this.relatedIssues.length; + }, + hasHelpPath() { + return this.helpPath.length > 0; + }, + issuableTypeIcon() { + return issuableIconMap[this.issuableType]; + }, + qaClass() { + return issuableQaClassMap[this.issuableType]; + }, + }, + linkedIssueTypesTextMap, +}; +</script> + +<template> + <div id="related-issues" class="related-issues-block"> + <div class="card card-slim gl-overflow-hidden"> + <div + :class="{ 'panel-empty-heading border-bottom-0': !hasBody }" + class="card-header gl-display-flex gl-justify-content-space-between" + > + <h3 + class="card-title h5 position-relative gl-my-0 gl-display-flex gl-align-items-center gl-h-7" + > + <gl-link + id="user-content-related-issues" + class="anchor position-absolute gl-text-decoration-none" + href="#related-issues" + aria-hidden="true" + /> + <slot name="headerText">{{ __('Linked issues') }}</slot> + <gl-link + v-if="hasHelpPath" + :href="helpPath" + target="_blank" + class="gl-display-flex gl-align-items-center gl-ml-2 gl-text-gray-500" + :aria-label="__('Read more about related issues')" + > + <gl-icon name="question" :size="12" role="text" /> + </gl-link> + + <div class="gl-display-inline-flex"> + <div class="js-related-issues-header-issue-count gl-display-inline-flex gl-mx-5"> + <span class="gl-display-inline-flex gl-align-items-center"> + <gl-icon :name="issuableTypeIcon" class="gl-mr-2 gl-text-gray-500" /> + {{ badgeLabel }} + </span> + </div> + <gl-button + v-if="canAdmin" + data-qa-selector="related_issues_plus_button" + icon="plus" + :aria-label="__('Add a related issue')" + :class="qaClass" + class="js-issue-count-badge-add-button" + @click="$emit('toggleAddRelatedIssuesForm', $event)" + /> + </div> + </h3> + <slot name="headerActions"></slot> + </div> + <div + class="linked-issues-card-body bg-gray-light" + :class="{ + 'gl-p-5': isFormVisible || shouldShowTokenBody, + }" + > + <div + v-if="isFormVisible" + class="js-add-related-issues-form-area card-body bordered-box bg-white" + > + <add-issuable-form + :show-categorized-issues="showCategorizedIssues" + :is-submitting="isSubmitting" + :issuable-type="issuableType" + :input-value="inputValue" + :pending-references="pendingReferences" + :auto-complete-sources="autoCompleteSources" + :path-id-separator="pathIdSeparator" + @pendingIssuableRemoveRequest="$emit('pendingIssuableRemoveRequest', $event)" + @addIssuableFormInput="$emit('addIssuableFormInput', $event)" + @addIssuableFormBlur="$emit('addIssuableFormBlur', $event)" + @addIssuableFormSubmit="$emit('addIssuableFormSubmit', $event)" + @addIssuableFormCancel="$emit('addIssuableFormCancel', $event)" + /> + </div> + <template v-if="shouldShowTokenBody"> + <related-issues-list + v-for="category in categorisedIssues" + :key="category.linkType" + :heading="$options.linkedIssueTypesTextMap[category.linkType]" + :can-admin="canAdmin" + :can-reorder="canReorder" + :is-fetching="isFetching" + :issuable-type="issuableType" + :path-id-separator="pathIdSeparator" + :related-issues="category.issues" + @relatedIssueRemoveRequest="$emit('relatedIssueRemoveRequest', $event)" + @saveReorder="$emit('saveReorder', $event)" + /> + </template> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/related_issues/components/related_issues_list.vue b/app/assets/javascripts/related_issues/components/related_issues_list.vue new file mode 100644 index 00000000000..a75fe4397bb --- /dev/null +++ b/app/assets/javascripts/related_issues/components/related_issues_list.vue @@ -0,0 +1,146 @@ +<script> +import { GlLoadingIcon } from '@gitlab/ui'; +import Sortable from 'sortablejs'; +import sortableConfig from 'ee_else_ce/sortable/sortable_config'; +import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; + +export default { + name: 'RelatedIssuesList', + directives: { + tooltip, + }, + components: { + GlLoadingIcon, + RelatedIssuableItem, + }, + props: { + canAdmin: { + type: Boolean, + required: false, + default: false, + }, + canReorder: { + type: Boolean, + required: false, + default: false, + }, + heading: { + type: String, + required: false, + default: '', + }, + isFetching: { + type: Boolean, + required: false, + default: false, + }, + issuableType: { + type: String, + required: true, + }, + pathIdSeparator: { + type: String, + required: true, + }, + relatedIssues: { + type: Array, + required: false, + default: () => [], + }, + }, + mounted() { + if (this.canReorder) { + this.sortable = Sortable.create(this.$refs.list, { + ...sortableConfig, + onStart: this.addDraggingCursor, + onEnd: this.reordered, + }); + } + }, + methods: { + getBeforeAfterId(itemEl) { + const prevItemEl = itemEl.previousElementSibling; + const nextItemEl = itemEl.nextElementSibling; + + return { + beforeId: prevItemEl && parseInt(prevItemEl.dataset.orderingId, 0), + afterId: nextItemEl && parseInt(nextItemEl.dataset.orderingId, 0), + }; + }, + reordered(event) { + this.removeDraggingCursor(); + const { beforeId, afterId } = this.getBeforeAfterId(event.item); + const { oldIndex, newIndex } = event; + + this.$emit('saveReorder', { + issueId: parseInt(event.item.dataset.key, 10), + oldIndex, + newIndex, + afterId, + beforeId, + }); + }, + addDraggingCursor() { + document.body.classList.add('is-dragging'); + }, + removeDraggingCursor() { + document.body.classList.remove('is-dragging'); + }, + issuableOrderingId({ epicIssueId, id }) { + return this.issuableType === 'issue' ? epicIssueId : id; + }, + }, +}; +</script> + +<template> + <div> + <h4 v-if="heading" class="gl-font-base mt-0">{{ heading }}</h4> + <div + class="related-issues-token-body bordered-box bg-white" + :class="{ 'sortable-container': canReorder }" + > + <div v-if="isFetching" class="related-issues-loading-icon qa-related-issues-loading-icon"> + <gl-loading-icon ref="loadingIcon" label="Fetching linked issues" class="gl-mt-2" /> + </div> + <ul ref="list" :class="{ 'content-list': !canReorder }" class="related-items-list"> + <li + v-for="issue in relatedIssues" + :key="issue.id" + :class="{ + 'user-can-drag': canReorder, + 'sortable-row': canReorder, + 'card card-slim': canReorder, + }" + :data-key="issue.id" + :data-ordering-id="issuableOrderingId(issue)" + class="js-related-issues-token-list-item list-item pt-0 pb-0" + > + <related-issuable-item + :id-key="issue.id" + :display-reference="issue.reference" + :confidential="issue.confidential" + :title="issue.title" + :path="issue.path" + :state="issue.state" + :milestone="issue.milestone" + :assignees="issue.assignees" + :created-at="issue.createdAt" + :closed-at="issue.closedAt" + :weight="issue.weight" + :due-date="issue.dueDate" + :can-remove="canAdmin" + :can-reorder="canReorder" + :path-id-separator="pathIdSeparator" + :is-locked="issue.lockIssueRemoval" + :locked-message="issue.lockedMessage" + event-namespace="relatedIssue" + class="qa-related-issuable-item" + @relatedIssueRemoveRequest="$emit('relatedIssueRemoveRequest', $event)" + /> + </li> + </ul> + </div> + </div> +</template> diff --git a/app/assets/javascripts/related_issues/components/related_issues_root.vue b/app/assets/javascripts/related_issues/components/related_issues_root.vue new file mode 100644 index 00000000000..6f68b25b6fb --- /dev/null +++ b/app/assets/javascripts/related_issues/components/related_issues_root.vue @@ -0,0 +1,247 @@ +<script> +/* +`rawReferences` are separated by spaces. +Given `abc 123 zxc`, `rawReferences = ['abc', '123', 'zxc']` + +Consider you are typing `abc 123 zxc` in the input and your caret position is +at position 4 right before the `123` `rawReference`. Then you type `#` and +it becomes a valid reference, `#123`, but we don't want to jump it straight into +`pendingReferences` because you could still want to type. Say you typed `999` +and now we have `#999123`. Only when you move your caret away from that `rawReference` +do we actually put it in the `pendingReferences`. + +Your caret can stop touching a `rawReference` can happen in a variety of ways: + + - As you type, we only tokenize after you type a space or move with the arrow keys + - On blur, we consider your caret not touching anything + +--- + + - When you click the "Add related issues"(in the `AddIssuableForm`), + we submit the `pendingReferences` to the server and they come back as actual `relatedIssues` + - When you click the "Cancel"(in the `AddIssuableForm`), we clear out `pendingReferences` + and hide the `AddIssuableForm` area. + +*/ +import { deprecatedCreateFlash as Flash } from '~/flash'; +import { __ } from '~/locale'; +import RelatedIssuesBlock from './related_issues_block.vue'; +import RelatedIssuesStore from '../stores/related_issues_store'; +import RelatedIssuesService from '../services/related_issues_service'; +import { + relatedIssuesRemoveErrorMap, + pathIndeterminateErrorMap, + addRelatedIssueErrorMap, + issuableTypesMap, + PathIdSeparator, +} from '../constants'; + +export default { + name: 'RelatedIssuesRoot', + components: { + relatedIssuesBlock: RelatedIssuesBlock, + }, + props: { + endpoint: { + type: String, + required: true, + }, + canAdmin: { + type: Boolean, + required: false, + default: false, + }, + canReorder: { + type: Boolean, + required: false, + default: false, + }, + helpPath: { + type: String, + required: false, + default: '', + }, + issuableType: { + type: String, + required: false, + default: issuableTypesMap.ISSUE, + }, + allowAutoComplete: { + type: Boolean, + required: false, + default: true, + }, + pathIdSeparator: { + type: String, + required: false, + default: PathIdSeparator.Issue, + }, + cssClass: { + type: String, + required: false, + default: '', + }, + showCategorizedIssues: { + type: Boolean, + required: false, + default: true, + }, + }, + data() { + this.store = new RelatedIssuesStore(); + + return { + state: this.store.state, + isFetching: false, + isSubmitting: false, + isFormVisible: false, + inputValue: '', + }; + }, + computed: { + autoCompleteSources() { + if (!this.allowAutoComplete) return {}; + return gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources; + }, + }, + created() { + this.service = new RelatedIssuesService(this.endpoint); + this.fetchRelatedIssues(); + }, + methods: { + findRelatedIssueById(id) { + return this.state.relatedIssues.find(issue => issue.id === id); + }, + onRelatedIssueRemoveRequest(idToRemove) { + const issueToRemove = this.findRelatedIssueById(idToRemove); + + if (issueToRemove) { + RelatedIssuesService.remove(issueToRemove.relationPath) + .then(({ data }) => { + this.store.setRelatedIssues(data.issuables); + }) + .catch(res => { + if (res && res.status !== 404) { + Flash(relatedIssuesRemoveErrorMap[this.issuableType]); + } + }); + } else { + Flash(pathIndeterminateErrorMap[this.issuableType]); + } + }, + onToggleAddRelatedIssuesForm() { + this.isFormVisible = !this.isFormVisible; + }, + onPendingIssueRemoveRequest(indexToRemove) { + this.store.removePendingRelatedIssue(indexToRemove); + }, + onPendingFormSubmit(event) { + this.processAllReferences(event.pendingReferences); + + if (this.state.pendingReferences.length > 0) { + this.isSubmitting = true; + this.service + .addRelatedIssues(this.state.pendingReferences, event.linkedIssueType) + .then(({ data }) => { + // We could potentially lose some pending issues in the interim here + this.store.setPendingReferences([]); + this.store.setRelatedIssues(data.issuables); + + // Close the form on submission + this.isFormVisible = false; + }) + .catch(({ response }) => { + let errorMessage = addRelatedIssueErrorMap[this.issuableType]; + if (response && response.data && response.data.message) { + errorMessage = response.data.message; + } + Flash(errorMessage); + }) + .finally(() => { + this.isSubmitting = false; + }); + } + }, + onPendingFormCancel() { + this.isFormVisible = false; + this.store.setPendingReferences([]); + this.inputValue = ''; + }, + fetchRelatedIssues() { + this.isFetching = true; + this.service + .fetchRelatedIssues() + .then(({ data }) => { + this.store.setRelatedIssues(data); + }) + .catch(() => { + this.store.setRelatedIssues([]); + Flash(__('An error occurred while fetching issues.')); + }) + .finally(() => { + this.isFetching = false; + }); + }, + saveIssueOrder({ issueId, beforeId, afterId, oldIndex, newIndex }) { + const issueToReorder = this.findRelatedIssueById(issueId); + + if (issueToReorder) { + RelatedIssuesService.saveOrder({ + endpoint: issueToReorder.relationPath, + move_before_id: beforeId, + move_after_id: afterId, + }) + .then(({ data }) => { + if (!data.message) { + this.store.updateIssueOrder(oldIndex, newIndex); + } + }) + .catch(() => { + Flash(__('An error occurred while reordering issues.')); + }); + } + }, + onInput({ untouchedRawReferences, touchedReference }) { + this.store.addPendingReferences(untouchedRawReferences); + + this.inputValue = `${touchedReference}`; + }, + onBlur(newValue) { + this.processAllReferences(newValue); + }, + processAllReferences(value = '') { + const rawReferences = value.split(/\s+/).filter(reference => reference.trim().length > 0); + + this.store.addPendingReferences(rawReferences); + this.inputValue = ''; + }, + }, +}; +</script> + +<template> + <related-issues-block + :class="cssClass" + :help-path="helpPath" + :is-fetching="isFetching" + :is-submitting="isSubmitting" + :related-issues="state.relatedIssues" + :can-admin="canAdmin" + :can-reorder="canReorder" + :pending-references="state.pendingReferences" + :is-form-visible="isFormVisible" + :input-value="inputValue" + :auto-complete-sources="autoCompleteSources" + :issuable-type="issuableType" + :path-id-separator="pathIdSeparator" + :show-categorized-issues="showCategorizedIssues" + @saveReorder="saveIssueOrder" + @toggleAddRelatedIssuesForm="onToggleAddRelatedIssuesForm" + @addIssuableFormInput="onInput" + @addIssuableFormBlur="onBlur" + @addIssuableFormSubmit="onPendingFormSubmit" + @addIssuableFormCancel="onPendingFormCancel" + @pendingIssuableRemoveRequest="onPendingIssueRemoveRequest" + @relatedIssueRemoveRequest="onRelatedIssueRemoveRequest" + /> +</template> diff --git a/app/assets/javascripts/related_issues/constants.js b/app/assets/javascripts/related_issues/constants.js new file mode 100644 index 00000000000..89eae069a24 --- /dev/null +++ b/app/assets/javascripts/related_issues/constants.js @@ -0,0 +1,106 @@ +import { __, sprintf } from '~/locale'; + +export const issuableTypesMap = { + ISSUE: 'issue', + EPIC: 'epic', + MERGE_REQUEST: 'merge_request', +}; + +export const linkedIssueTypesMap = { + BLOCKS: 'blocks', + IS_BLOCKED_BY: 'is_blocked_by', + RELATES_TO: 'relates_to', +}; + +export const linkedIssueTypesTextMap = { + [linkedIssueTypesMap.RELATES_TO]: __('Relates to'), + [linkedIssueTypesMap.BLOCKS]: __('Blocks'), + [linkedIssueTypesMap.IS_BLOCKED_BY]: __('Is blocked by'), +}; + +export const autoCompleteTextMap = { + true: { + [issuableTypesMap.ISSUE]: sprintf( + __(' or %{emphasisStart}#issue id%{emphasisEnd}'), + { emphasisStart: '<', emphasisEnd: '>' }, + false, + ), + [issuableTypesMap.EPIC]: sprintf( + __(' or %{emphasisStart}&epic id%{emphasisEnd}'), + { emphasisStart: '<', emphasisEnd: '>' }, + false, + ), + [issuableTypesMap.MERGE_REQUEST]: sprintf( + __(' or %{emphasisStart}!merge request id%{emphasisEnd}'), + { emphasisStart: '<', emphasisEnd: '>' }, + false, + ), + }, + false: { + [issuableTypesMap.ISSUE]: '', + [issuableTypesMap.EPIC]: '', + [issuableTypesMap.MERGE_REQUEST]: __(' or references (e.g. path/to/project!merge_request_id)'), + }, +}; + +export const inputPlaceholderTextMap = { + [issuableTypesMap.ISSUE]: __('Paste issue link'), + [issuableTypesMap.EPIC]: __('Paste epic link'), + [issuableTypesMap.MERGE_REQUEST]: __('Enter merge request URLs'), +}; + +export const inputPlaceholderConfidentialTextMap = { + [issuableTypesMap.ISSUE]: __('Paste confidential issue link'), + [issuableTypesMap.EPIC]: __('Paste confidential epic link'), + [issuableTypesMap.MERGE_REQUEST]: __('Enter merge request URLs'), +}; + +export const relatedIssuesRemoveErrorMap = { + [issuableTypesMap.ISSUE]: __('An error occurred while removing issues.'), + [issuableTypesMap.EPIC]: __('An error occurred while removing epics.'), +}; + +export const pathIndeterminateErrorMap = { + [issuableTypesMap.ISSUE]: __('We could not determine the path to remove the issue'), + [issuableTypesMap.EPIC]: __('We could not determine the path to remove the epic'), +}; + +export const itemAddFailureTypesMap = { + NOT_FOUND: 'not_found', + MAX_NUMBER_OF_CHILD_EPICS: 'conflict', +}; + +export const addRelatedIssueErrorMap = { + [issuableTypesMap.ISSUE]: __('Issue cannot be found.'), + [issuableTypesMap.EPIC]: __('Epic cannot be found.'), +}; + +export const addRelatedItemErrorMap = { + [itemAddFailureTypesMap.MAX_NUMBER_OF_CHILD_EPICS]: __( + 'This epic already has the maximum number of child epics.', + ), +}; + +/** + * These are used to map issuableType to the correct icon. + * Since these are never used for any display purposes, don't wrap + * them inside i18n functions. + */ +export const issuableIconMap = { + [issuableTypesMap.ISSUE]: 'issues', + [issuableTypesMap.EPIC]: 'epic', +}; + +/** + * These are used to map issuableType to the correct QA class. + * Since these are never used for any display purposes, don't wrap + * them inside i18n functions. + */ +export const issuableQaClassMap = { + [issuableTypesMap.EPIC]: 'qa-add-epics-button', +}; + +export const PathIdSeparator = { + Epic: '&', + Issue: '#', +}; diff --git a/app/assets/javascripts/related_issues/index.js b/app/assets/javascripts/related_issues/index.js new file mode 100644 index 00000000000..2e8626890cb --- /dev/null +++ b/app/assets/javascripts/related_issues/index.js @@ -0,0 +1,27 @@ +import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import RelatedIssuesRoot from './components/related_issues_root.vue'; + +export default function initRelatedIssues() { + const relatedIssuesRootElement = document.querySelector('.js-related-issues-root'); + if (relatedIssuesRootElement) { + // eslint-disable-next-line no-new + new Vue({ + el: relatedIssuesRootElement, + components: { + relatedIssuesRoot: RelatedIssuesRoot, + }, + render: createElement => + createElement('related-issues-root', { + props: { + endpoint: relatedIssuesRootElement.dataset.endpoint, + canAdmin: parseBoolean(relatedIssuesRootElement.dataset.canAddRelatedIssues), + helpPath: relatedIssuesRootElement.dataset.helpPath, + showCategorizedIssues: parseBoolean( + relatedIssuesRootElement.dataset.showCategorizedIssues, + ), + }, + }), + }); + } +} diff --git a/app/assets/javascripts/related_issues/services/related_issues_service.js b/app/assets/javascripts/related_issues/services/related_issues_service.js new file mode 100644 index 00000000000..3c19f63157e --- /dev/null +++ b/app/assets/javascripts/related_issues/services/related_issues_service.js @@ -0,0 +1,34 @@ +import axios from '~/lib/utils/axios_utils'; +import { linkedIssueTypesMap } from '../constants'; + +class RelatedIssuesService { + constructor(endpoint) { + this.endpoint = endpoint; + } + + fetchRelatedIssues() { + return axios.get(this.endpoint); + } + + addRelatedIssues(newIssueReferences, linkType = linkedIssueTypesMap.RELATES_TO) { + return axios.post(this.endpoint, { + issuable_references: newIssueReferences, + link_type: linkType, + }); + } + + static saveOrder({ endpoint, move_before_id, move_after_id }) { + return axios.put(endpoint, { + epic: { + move_before_id, + move_after_id, + }, + }); + } + + static remove(endpoint) { + return axios.delete(endpoint); + } +} + +export default RelatedIssuesService; diff --git a/app/assets/javascripts/related_issues/stores/related_issues_store.js b/app/assets/javascripts/related_issues/stores/related_issues_store.js new file mode 100644 index 00000000000..14d71628cad --- /dev/null +++ b/app/assets/javascripts/related_issues/stores/related_issues_store.js @@ -0,0 +1,50 @@ +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; + +class RelatedIssuesStore { + constructor() { + this.state = { + // Stores issue objects of the known related issues + relatedIssues: [], + // Stores references of the "staging area" related issues that are planned to be added + pendingReferences: [], + }; + } + + setRelatedIssues(issues = []) { + this.state.relatedIssues = convertObjectPropsToCamelCase(issues, { deep: true }); + } + + addRelatedIssues(issues = []) { + this.setRelatedIssues(this.state.relatedIssues.concat(issues)); + } + + removeRelatedIssue(issue) { + this.state.relatedIssues = this.state.relatedIssues.filter(x => x.id !== issue.id); + } + + updateIssueOrder(oldIndex, newIndex) { + if (this.state.relatedIssues.length > 0) { + const updatedIssue = this.state.relatedIssues.splice(oldIndex, 1)[0]; + this.state.relatedIssues.splice(newIndex, 0, updatedIssue); + } + } + + setPendingReferences(issues) { + // Remove duplicates but retain order. + // If you don't do this, Vue will be confused by duplicates and refuse to delete them all. + this.state.pendingReferences = issues.filter((ref, idx) => issues.indexOf(ref) === idx); + } + + addPendingReferences(references = []) { + const issues = this.state.pendingReferences.concat(references); + this.setPendingReferences(issues); + } + + removePendingRelatedIssue(indexToRemove) { + this.state.pendingReferences = this.state.pendingReferences.filter( + (reference, index) => index !== indexToRemove, + ); + } +} + +export default RelatedIssuesStore; diff --git a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue index 15e9b8559d4..7db76ed576c 100644 --- a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue +++ b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue @@ -1,15 +1,14 @@ <script> import { mapState, mapActions } from 'vuex'; -import { GlLink, GlLoadingIcon } from '@gitlab/ui'; +import { GlLink, GlLoadingIcon, GlIcon } from '@gitlab/ui'; import { sprintf, n__, s__ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue'; import { parseIssuableData } from '../../issue_show/utils/parse_data'; export default { name: 'RelatedMergeRequests', components: { - Icon, + GlIcon, GlLink, GlLoadingIcon, RelatedIssuableItem, @@ -85,7 +84,7 @@ export default { <div class="mr-count-badge gl-display-inline-flex"> <div class="mr-count-badge-count"> <svg class="s16 mr-1 text-secondary"> - <icon name="merge-request" class="mr-1 text-secondary" /> + <gl-icon name="merge-request" class="mr-1 text-secondary" /> </svg> <span class="js-items-count">{{ totalCount }}</span> </div> diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue index 7b7c80a6269..e1edf3d689d 100644 --- a/app/assets/javascripts/releases/components/app_edit_new.vue +++ b/app/assets/javascripts/releases/components/app_edit_new.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { mapState, mapActions, mapGetters } from 'vuex'; import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; @@ -139,7 +140,7 @@ export default { class="form-control" /> </gl-form-group> - <gl-form-group class="w-50" @keydown.enter.prevent.capture> + <gl-form-group class="w-50" data-testid="milestones-field"> <label>{{ __('Milestones') }}</label> <div class="d-flex flex-column col-md-6 col-sm-10 pl-0"> <milestone-combobox diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue index 67085ecca2b..b8cf6ce478f 100644 --- a/app/assets/javascripts/releases/components/app_index.vue +++ b/app/assets/javascripts/releases/components/app_index.vue @@ -1,6 +1,11 @@ <script> import { mapState, mapActions } from 'vuex'; -import { GlSkeletonLoading, GlEmptyState, GlLink, GlButton } from '@gitlab/ui'; +import { + GlDeprecatedSkeletonLoading as GlSkeletonLoading, + GlEmptyState, + GlLink, + GlButton, +} from '@gitlab/ui'; import { getParameterByName, historyPushState, @@ -20,27 +25,16 @@ export default { GlLink, GlButton, }, - props: { - projectId: { - type: String, - required: true, - }, - documentationPath: { - type: String, - required: true, - }, - illustrationPath: { - type: String, - required: true, - }, - newReleasePath: { - type: String, - required: false, - default: '', - }, - }, computed: { - ...mapState('list', ['isLoading', 'releases', 'hasError', 'pageInfo']), + ...mapState('list', [ + 'documentationPath', + 'illustrationPath', + 'newReleasePath', + 'isLoading', + 'releases', + 'hasError', + 'pageInfo', + ]), shouldRenderEmptyState() { return !this.releases.length && !this.hasError && !this.isLoading; }, @@ -56,14 +50,13 @@ export default { created() { this.fetchReleases({ page: getParameterByName('page'), - projectId: this.projectId, }); }, methods: { ...mapActions('list', ['fetchReleases']), onChangePage(page) { historyPushState(buildUrlWithCurrentLocation(`?page=${page}`)); - this.fetchReleases({ page, projectId: this.projectId }); + this.fetchReleases({ page }); }, }, }; diff --git a/app/assets/javascripts/releases/components/app_show.vue b/app/assets/javascripts/releases/components/app_show.vue index 0e65d722952..8b89f0cf3fc 100644 --- a/app/assets/javascripts/releases/components/app_show.vue +++ b/app/assets/javascripts/releases/components/app_show.vue @@ -1,6 +1,6 @@ <script> import { mapState, mapActions } from 'vuex'; -import { GlSkeletonLoading } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import ReleaseBlock from './release_block.vue'; export default { diff --git a/app/assets/javascripts/releases/components/evidence_block.vue b/app/assets/javascripts/releases/components/evidence_block.vue index 6468e2ded62..3724162f6d5 100644 --- a/app/assets/javascripts/releases/components/evidence_block.vue +++ b/app/assets/javascripts/releases/components/evidence_block.vue @@ -80,9 +80,7 @@ export default { <span class="js-short monospace">{{ shortSha(index) }}</span> </template> <template #expanded> - <span class="js-expanded monospace gl-pl-1-deprecated-no-really-do-not-use-me">{{ - sha(index) - }}</span> + <span class="js-expanded monospace gl-pl-2">{{ sha(index) }}</span> </template> </expand-button> <clipboard-button diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue index e0061d88ccb..2629df08be7 100644 --- a/app/assets/javascripts/releases/components/release_block.vue +++ b/app/assets/javascripts/releases/components/release_block.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { isEmpty } from 'lodash'; import $ from 'jquery'; import { slugify } from '~/lib/utils/text_utility'; diff --git a/app/assets/javascripts/releases/components/release_block_assets.vue b/app/assets/javascripts/releases/components/release_block_assets.vue index 9583f5737df..8824cbefd7e 100644 --- a/app/assets/javascripts/releases/components/release_block_assets.vue +++ b/app/assets/javascripts/releases/components/release_block_assets.vue @@ -1,7 +1,6 @@ <script> import { GlTooltipDirective, GlLink, GlButton, GlCollapse, GlIcon, GlBadge } from '@gitlab/ui'; import { difference, get } from 'lodash'; -import Icon from '~/vue_shared/components/icon.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { ASSET_LINK_TYPE } from '../constants'; import { __, s__, sprintf } from '~/locale'; @@ -13,7 +12,6 @@ export default { GlButton, GlCollapse, GlIcon, - Icon, GlBadge, }, directives: { @@ -157,7 +155,7 @@ export default { <ul v-if="assets.links.length" class="pl-0 mb-0 gl-mt-3 list-unstyled js-assets-list"> <li v-for="link in assets.links" :key="link.name" class="gl-mb-3"> <gl-link v-gl-tooltip.bottom :title="__('Download asset')" :href="link.directAssetUrl"> - <icon name="package" class="align-middle gl-mr-2 align-text-bottom" /> + <gl-icon name="package" class="align-middle gl-mr-2 align-text-bottom" /> {{ link.name }} <span v-if="link.external" data-testid="external-link-indicator">{{ __('(external source)') @@ -174,9 +172,9 @@ export default { aria-haspopup="true" aria-expanded="false" > - <icon name="doc-code" class="align-top gl-mr-2" /> + <gl-icon name="doc-code" class="align-top gl-mr-2" /> {{ __('Source code') }} - <icon name="chevron-down" /> + <gl-icon name="chevron-down" /> </button> <div class="js-sources-dropdown dropdown-menu"> diff --git a/app/assets/javascripts/releases/components/release_block_footer.vue b/app/assets/javascripts/releases/components/release_block_footer.vue index 26154272d39..3beec466c54 100644 --- a/app/assets/javascripts/releases/components/release_block_footer.vue +++ b/app/assets/javascripts/releases/components/release_block_footer.vue @@ -1,6 +1,5 @@ <script> -import { GlTooltipDirective, GlLink } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; import { __, sprintf } from '~/locale'; @@ -8,7 +7,7 @@ import { __, sprintf } from '~/locale'; export default { name: 'ReleaseBlockFooter', components: { - Icon, + GlIcon, GlLink, UserAvatarLink, }, @@ -68,7 +67,7 @@ export default { <template> <div> <div v-if="commit" class="float-left mr-3 d-flex align-items-center js-commit-info"> - <icon ref="commitIcon" name="commit" class="mr-1" /> + <gl-icon ref="commitIcon" name="commit" class="mr-1" /> <div v-gl-tooltip.bottom :title="commit.title"> <gl-link v-if="commitPath" :href="commitPath"> {{ commit.shortId }} @@ -78,7 +77,7 @@ export default { </div> <div v-if="tagName" class="float-left mr-3 d-flex align-items-center js-tag-info"> - <icon name="tag" class="mr-1" /> + <gl-icon name="tag" class="mr-1" /> <div v-gl-tooltip.bottom :title="__('Tag')"> <gl-link v-if="tagPath" :href="tagPath"> {{ tagName }} diff --git a/app/assets/javascripts/releases/components/release_block_header.vue b/app/assets/javascripts/releases/components/release_block_header.vue index 310fba0fe76..95292a26bce 100644 --- a/app/assets/javascripts/releases/components/release_block_header.vue +++ b/app/assets/javascripts/releases/components/release_block_header.vue @@ -1,6 +1,5 @@ <script> -import { GlTooltipDirective, GlLink, GlBadge, GlButton } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlTooltipDirective, GlLink, GlBadge, GlButton, GlIcon } from '@gitlab/ui'; import { BACK_URL_PARAM } from '~/releases/constants'; import { setUrlParams } from '~/lib/utils/url_utility'; @@ -9,7 +8,7 @@ export default { components: { GlLink, GlBadge, - Icon, + GlIcon, GlButton, }, directives: { @@ -42,7 +41,7 @@ export default { <template> <div class="card-header d-flex align-items-center bg-white pr-0"> - <h2 class="card-title my-2 mr-auto gl-font-size-20-deprecated-no-really-do-not-use-me"> + <h2 class="card-title my-2 mr-auto"> <gl-link v-if="selfLink" :href="selfLink" class="font-size-inherit"> {{ release.name }} </gl-link> @@ -60,7 +59,7 @@ export default { :title="__('Edit this release')" :href="editLink" > - <icon name="pencil" /> + <gl-icon name="pencil" /> </gl-button> </div> </template> diff --git a/app/assets/javascripts/releases/components/release_block_metadata.vue b/app/assets/javascripts/releases/components/release_block_metadata.vue index 861c2e11798..2247b4c0064 100644 --- a/app/assets/javascripts/releases/components/release_block_metadata.vue +++ b/app/assets/javascripts/releases/components/release_block_metadata.vue @@ -1,7 +1,6 @@ <script> -import { GlTooltipDirective, GlLink } from '@gitlab/ui'; +import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; import ReleaseBlockAuthor from './release_block_author.vue'; import ReleaseBlockMilestones from './release_block_milestones.vue'; @@ -9,7 +8,7 @@ import ReleaseBlockMilestones from './release_block_milestones.vue'; export default { name: 'ReleaseBlockMetadata', components: { - Icon, + GlIcon, GlLink, ReleaseBlockAuthor, ReleaseBlockMilestones, @@ -58,7 +57,7 @@ export default { <template> <div class="card-subtitle d-flex flex-wrap text-secondary"> <div class="gl-mr-3"> - <icon name="commit" class="align-middle" /> + <gl-icon name="commit" class="align-middle" /> <gl-link v-if="commitUrl" v-gl-tooltip.bottom :title="commit.title" :href="commitUrl"> {{ commit.shortId }} </gl-link> @@ -66,7 +65,7 @@ export default { </div> <div class="gl-mr-3"> - <icon name="tag" class="align-middle" /> + <gl-icon name="tag" class="align-middle" /> <gl-link v-if="tagUrl" v-gl-tooltip.bottom :title="__('Tag')" :href="tagUrl"> {{ release.tagName }} </gl-link> diff --git a/app/assets/javascripts/releases/components/release_block_milestones.vue b/app/assets/javascripts/releases/components/release_block_milestones.vue index 9abd3345b22..1da683764b3 100644 --- a/app/assets/javascripts/releases/components/release_block_milestones.vue +++ b/app/assets/javascripts/releases/components/release_block_milestones.vue @@ -1,13 +1,12 @@ <script> -import { GlTooltipDirective, GlLink } from '@gitlab/ui'; +import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui'; import { n__ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; export default { name: 'ReleaseBlockMilestones', components: { GlLink, - Icon, + GlIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -29,7 +28,7 @@ export default { <template> <div> <div class="js-milestone-list-label"> - <icon name="flag" class="align-middle" /> + <gl-icon name="flag" class="align-middle" /> <span class="js-label-text">{{ labelText }}</span> </div> diff --git a/app/assets/javascripts/releases/components/releases_pagination.vue b/app/assets/javascripts/releases/components/releases_pagination.vue new file mode 100644 index 00000000000..062c72b445b --- /dev/null +++ b/app/assets/javascripts/releases/components/releases_pagination.vue @@ -0,0 +1,20 @@ +<script> +import { mapGetters } from 'vuex'; +import ReleasesPaginationGraphql from './releases_pagination_graphql.vue'; +import ReleasesPaginationRest from './releases_pagination_rest.vue'; + +export default { + name: 'ReleasesPagination', + components: { ReleasesPaginationGraphql, ReleasesPaginationRest }, + computed: { + ...mapGetters(['useGraphQLEndpoint']), + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-justify-content-center"> + <releases-pagination-graphql v-if="useGraphQLEndpoint" /> + <releases-pagination-rest v-else /> + </div> +</template> diff --git a/app/assets/javascripts/releases/components/releases_pagination_graphql.vue b/app/assets/javascripts/releases/components/releases_pagination_graphql.vue new file mode 100644 index 00000000000..a4fe407a5bd --- /dev/null +++ b/app/assets/javascripts/releases/components/releases_pagination_graphql.vue @@ -0,0 +1,35 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import { GlKeysetPagination } from '@gitlab/ui'; +import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils'; + +export default { + name: 'ReleasesPaginationGraphql', + components: { GlKeysetPagination }, + computed: { + ...mapState('list', ['graphQlPageInfo']), + showPagination() { + return this.graphQlPageInfo.hasPreviousPage || this.graphQlPageInfo.hasNextPage; + }, + }, + methods: { + ...mapActions('list', ['fetchReleasesGraphQl']), + onPrev(before) { + historyPushState(buildUrlWithCurrentLocation(`?before=${before}`)); + this.fetchReleasesGraphQl({ before }); + }, + onNext(after) { + historyPushState(buildUrlWithCurrentLocation(`?after=${after}`)); + this.fetchReleasesGraphQl({ after }); + }, + }, +}; +</script> +<template> + <gl-keyset-pagination + v-if="showPagination" + v-bind="graphQlPageInfo" + @prev="onPrev($event)" + @next="onNext($event)" + /> +</template> diff --git a/app/assets/javascripts/releases/components/releases_pagination_rest.vue b/app/assets/javascripts/releases/components/releases_pagination_rest.vue new file mode 100644 index 00000000000..992cc4cd469 --- /dev/null +++ b/app/assets/javascripts/releases/components/releases_pagination_rest.vue @@ -0,0 +1,24 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; +import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils'; + +export default { + name: 'ReleasesPaginationRest', + components: { TablePagination }, + computed: { + ...mapState('list', ['pageInfo']), + }, + methods: { + ...mapActions('list', ['fetchReleasesRest']), + onChangePage(page) { + historyPushState(buildUrlWithCurrentLocation(`?page=${page}`)); + this.fetchReleasesRest({ page }); + }, + }, +}; +</script> + +<template> + <table-pagination :change="onChangePage" :page-info="pageInfo" /> +</template> diff --git a/app/assets/javascripts/releases/mount_edit.js b/app/assets/javascripts/releases/mount_edit.js index c7385b3c57f..623b18591a0 100644 --- a/app/assets/javascripts/releases/mount_edit.js +++ b/app/assets/javascripts/releases/mount_edit.js @@ -1,8 +1,11 @@ import Vue from 'vue'; +import Vuex from 'vuex'; import ReleaseEditNewApp from './components/app_edit_new.vue'; import createStore from './stores'; import createDetailModule from './stores/modules/detail'; +Vue.use(Vuex); + export default () => { const el = document.getElementById('js-edit-release-page'); diff --git a/app/assets/javascripts/releases/mount_index.js b/app/assets/javascripts/releases/mount_index.js index 5f0bf3b6459..cd4fa5c5df5 100644 --- a/app/assets/javascripts/releases/mount_index.js +++ b/app/assets/javascripts/releases/mount_index.js @@ -1,7 +1,10 @@ import Vue from 'vue'; +import Vuex from 'vuex'; import ReleaseListApp from './components/app_index.vue'; import createStore from './stores'; -import listModule from './stores/modules/list'; +import createListModule from './stores/modules/list'; + +Vue.use(Vuex); export default () => { const el = document.getElementById('js-releases-page'); @@ -10,12 +13,14 @@ export default () => { el, store: createStore({ modules: { - list: listModule, + list: createListModule(el.dataset), + }, + featureFlags: { + graphqlReleaseData: Boolean(gon.features?.graphqlReleaseData), + graphqlReleasesPage: Boolean(gon.features?.graphqlReleasesPage), + graphqlMilestoneStats: Boolean(gon.features?.graphqlMilestoneStats), }, }), - render: h => - h(ReleaseListApp, { - props: el.dataset, - }), + render: h => h(ReleaseListApp), }); }; diff --git a/app/assets/javascripts/releases/mount_new.js b/app/assets/javascripts/releases/mount_new.js index 68003f6a346..10725e47740 100644 --- a/app/assets/javascripts/releases/mount_new.js +++ b/app/assets/javascripts/releases/mount_new.js @@ -1,8 +1,11 @@ import Vue from 'vue'; +import Vuex from 'vuex'; import ReleaseEditNewApp from './components/app_edit_new.vue'; import createStore from './stores'; import createDetailModule from './stores/modules/detail'; +Vue.use(Vuex); + export default () => { const el = document.getElementById('js-new-release-page'); diff --git a/app/assets/javascripts/releases/mount_show.js b/app/assets/javascripts/releases/mount_show.js index 7ddc8e786c1..eef015ee0a6 100644 --- a/app/assets/javascripts/releases/mount_show.js +++ b/app/assets/javascripts/releases/mount_show.js @@ -1,8 +1,11 @@ import Vue from 'vue'; +import Vuex from 'vuex'; import ReleaseShowApp from './components/app_show.vue'; import createStore from './stores'; import createDetailModule from './stores/modules/detail'; +Vue.use(Vuex); + export default () => { const el = document.getElementById('js-show-release-page'); diff --git a/app/assets/javascripts/releases/queries/all_releases.query.graphql b/app/assets/javascripts/releases/queries/all_releases.query.graphql new file mode 100644 index 00000000000..7a99f32fdfa --- /dev/null +++ b/app/assets/javascripts/releases/queries/all_releases.query.graphql @@ -0,0 +1,69 @@ +query allReleases($fullPath: ID!) { + project(fullPath: $fullPath) { + releases(first: 20) { + count + nodes { + name + tagName + tagPath + descriptionHtml + releasedAt + upcomingRelease + assets { + count + sources { + nodes { + format + url + } + } + links { + nodes { + id + name + url + directAssetUrl + linkType + external + } + } + } + evidences { + nodes { + filepath + collectedAt + sha + } + } + links { + editUrl + issuesUrl + mergeRequestsUrl + selfUrl + } + commit { + sha + webUrl + title + } + author { + webUrl + avatarUrl + username + } + milestones { + nodes { + id + title + description + webPath + stats { + totalIssuesCount + closedIssuesCount + } + } + } + } + } + } +} diff --git a/app/assets/javascripts/releases/stores/index.js b/app/assets/javascripts/releases/stores/index.js index 7f211145ccf..b2e93d789d7 100644 --- a/app/assets/javascripts/releases/stores/index.js +++ b/app/assets/javascripts/releases/stores/index.js @@ -1,8 +1,5 @@ -import Vue from 'vue'; import Vuex from 'vuex'; -Vue.use(Vuex); - export default ({ modules, featureFlags }) => new Vuex.Store({ modules, diff --git a/app/assets/javascripts/releases/stores/modules/list/actions.js b/app/assets/javascripts/releases/stores/modules/list/actions.js index 90fba319e9f..945b093b983 100644 --- a/app/assets/javascripts/releases/stores/modules/list/actions.js +++ b/app/assets/javascripts/releases/stores/modules/list/actions.js @@ -7,6 +7,8 @@ import { parseIntPagination, convertObjectPropsToCamelCase, } from '~/lib/utils/common_utils'; +import allReleasesQuery from '~/releases/queries/all_releases.query.graphql'; +import { gqClient, convertGraphQLResponse } from '../../../util'; /** * Commits a mutation to update the state while the main endpoint is being requested. @@ -21,13 +23,31 @@ export const requestReleases = ({ commit }) => commit(types.REQUEST_RELEASES); * * @param {String} projectId */ -export const fetchReleases = ({ dispatch }, { page = '1', projectId }) => { +export const fetchReleases = ({ dispatch, rootState, state }, { page = '1' }) => { dispatch('requestReleases'); - api - .releases(projectId, { page }) - .then(response => dispatch('receiveReleasesSuccess', response)) - .catch(() => dispatch('receiveReleasesError')); + if ( + rootState.featureFlags.graphqlReleaseData && + rootState.featureFlags.graphqlReleasesPage && + rootState.featureFlags.graphqlMilestoneStats + ) { + gqClient + .query({ + query: allReleasesQuery, + variables: { + fullPath: state.projectPath, + }, + }) + .then(response => { + dispatch('receiveReleasesSuccess', convertGraphQLResponse(response)); + }) + .catch(() => dispatch('receiveReleasesError')); + } else { + api + .releases(state.projectId, { page }) + .then(response => dispatch('receiveReleasesSuccess', response)) + .catch(() => dispatch('receiveReleasesError')); + } }; export const receiveReleasesSuccess = ({ commit }, { data, headers }) => { diff --git a/app/assets/javascripts/releases/stores/modules/list/index.js b/app/assets/javascripts/releases/stores/modules/list/index.js index e4633b15a0c..0f97fa83ced 100644 --- a/app/assets/javascripts/releases/stores/modules/list/index.js +++ b/app/assets/javascripts/releases/stores/modules/list/index.js @@ -1,10 +1,10 @@ -import state from './state'; +import createState from './state'; import * as actions from './actions'; import mutations from './mutations'; -export default { +export default initialState => ({ namespaced: true, actions, mutations, - state, -}; + state: createState(initialState), +}); diff --git a/app/assets/javascripts/releases/stores/modules/list/state.js b/app/assets/javascripts/releases/stores/modules/list/state.js index c251f56c9c5..9fe313745fc 100644 --- a/app/assets/javascripts/releases/stores/modules/list/state.js +++ b/app/assets/javascripts/releases/stores/modules/list/state.js @@ -1,4 +1,16 @@ -export default () => ({ +export default ({ + projectId, + projectPath, + documentationPath, + illustrationPath, + newReleasePath = '', +}) => ({ + projectId, + projectPath, + documentationPath, + illustrationPath, + newReleasePath, + isLoading: false, hasError: false, releases: [], diff --git a/app/assets/javascripts/releases/util.js b/app/assets/javascripts/releases/util.js index 842a423b142..d7fac7a9b65 100644 --- a/app/assets/javascripts/releases/util.js +++ b/app/assets/javascripts/releases/util.js @@ -1,3 +1,6 @@ +import { pick } from 'lodash'; +import createGqClient, { fetchPolicies } from '~/lib/graphql'; +import { truncateSha } from '~/lib/utils/text_utility'; import { convertObjectPropsToCamelCase, convertObjectPropsToSnakeCase, @@ -39,3 +42,89 @@ export const apiJsonToRelease = json => { return release; }; + +export const gqClient = createGqClient({}, { fetchPolicy: fetchPolicies.NO_CACHE }); + +const convertScalarProperties = graphQLRelease => + pick(graphQLRelease, [ + 'name', + 'tagName', + 'tagPath', + 'descriptionHtml', + 'releasedAt', + 'upcomingRelease', + ]); + +const convertAssets = graphQLRelease => ({ + assets: { + count: graphQLRelease.assets.count, + sources: [...graphQLRelease.assets.sources.nodes], + links: graphQLRelease.assets.links.nodes.map(l => ({ + ...l, + linkType: l.linkType?.toLowerCase(), + })), + }, +}); + +const convertEvidences = graphQLRelease => ({ + evidences: graphQLRelease.evidences.nodes.map(e => e), +}); + +const convertLinks = graphQLRelease => ({ + _links: { + ...graphQLRelease.links, + self: graphQLRelease.links?.selfUrl, + }, +}); + +const convertCommit = graphQLRelease => { + if (!graphQLRelease.commit) { + return {}; + } + + return { + commit: { + shortId: truncateSha(graphQLRelease.commit.sha), + title: graphQLRelease.commit.title, + }, + commitPath: graphQLRelease.commit.webUrl, + }; +}; + +const convertAuthor = graphQLRelease => ({ author: graphQLRelease.author }); + +const convertMilestones = graphQLRelease => ({ + milestones: graphQLRelease.milestones.nodes.map(m => ({ + ...m, + webUrl: m.webPath, + webPath: undefined, + issueStats: { + total: m.stats.totalIssuesCount, + closed: m.stats.closedIssuesCount, + }, + stats: undefined, + })), +}); + +/** + * Converts the response from the GraphQL endpoint into the + * same shape as is returned from the Releases REST API. + * + * This allows the release components to use the response + * from either endpoint interchangeably. + * + * @param response The response received from the GraphQL endpoint + */ +export const convertGraphQLResponse = response => { + const releases = response.data.project.releases.nodes.map(r => ({ + ...convertScalarProperties(r), + ...convertAssets(r), + ...convertEvidences(r), + ...convertLinks(r), + ...convertCommit(r), + ...convertAuthor(r), + ...convertMilestones(r), + })); + + return { data: releases }; +}; diff --git a/app/assets/javascripts/reports/accessibility_report/store/index.js b/app/assets/javascripts/reports/accessibility_report/store/index.js index 047964260ad..9fa2c589324 100644 --- a/app/assets/javascripts/reports/accessibility_report/store/index.js +++ b/app/assets/javascripts/reports/accessibility_report/store/index.js @@ -7,10 +7,11 @@ import state from './state'; Vue.use(Vuex); -export default initialState => - new Vuex.Store({ - actions, - getters, - mutations, - state: state(initialState), - }); +export const getStoreConfig = initialState => ({ + actions, + getters, + mutations, + state: state(initialState), +}); + +export default initialState => new Vuex.Store(getStoreConfig(initialState)); diff --git a/app/assets/javascripts/reports/codequality_report/store/index.js b/app/assets/javascripts/reports/codequality_report/store/index.js index 047964260ad..9fa2c589324 100644 --- a/app/assets/javascripts/reports/codequality_report/store/index.js +++ b/app/assets/javascripts/reports/codequality_report/store/index.js @@ -7,10 +7,11 @@ import state from './state'; Vue.use(Vuex); -export default initialState => - new Vuex.Store({ - actions, - getters, - mutations, - state: state(initialState), - }); +export const getStoreConfig = initialState => ({ + actions, + getters, + mutations, + state: state(initialState), +}); + +export default initialState => new Vuex.Store(getStoreConfig(initialState)); diff --git a/app/assets/javascripts/reports/components/issue_status_icon.vue b/app/assets/javascripts/reports/components/issue_status_icon.vue index d79e3ddd798..bd41b8d23f1 100644 --- a/app/assets/javascripts/reports/components/issue_status_icon.vue +++ b/app/assets/javascripts/reports/components/issue_status_icon.vue @@ -1,11 +1,11 @@ <script> -import Icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '../constants'; export default { name: 'IssueStatusIcon', components: { - Icon, + GlIcon, }, props: { status: { @@ -49,6 +49,6 @@ export default { }" class="report-block-list-icon" > - <icon :name="iconName" :size="statusIconSize" :data-qa-selector="`status_${status}_icon`" /> + <gl-icon :name="iconName" :size="statusIconSize" :data-qa-selector="`status_${status}_icon`" /> </div> </template> diff --git a/app/assets/javascripts/reports/store/getters.js b/app/assets/javascripts/reports/store/getters.js index 6345be69f6f..d49e5760b3f 100644 --- a/app/assets/javascripts/reports/store/getters.js +++ b/app/assets/javascripts/reports/store/getters.js @@ -1,6 +1,5 @@ import { LOADING, ERROR, SUCCESS, STATUS_FAILED } from '../constants'; -// eslint-disable-next-line import/prefer-default-export export const summaryStatus = state => { if (state.isLoading) { return LOADING; diff --git a/app/assets/javascripts/reports/store/index.js b/app/assets/javascripts/reports/store/index.js index 467c692b438..a2edfa94a48 100644 --- a/app/assets/javascripts/reports/store/index.js +++ b/app/assets/javascripts/reports/store/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/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue index 368fa029d07..74437f286b4 100644 --- a/app/assets/javascripts/repository/components/breadcrumbs.vue +++ b/app/assets/javascripts/repository/components/breadcrumbs.vue @@ -4,10 +4,10 @@ import { GlDeprecatedDropdownDivider, GlDeprecatedDropdownHeader, GlDeprecatedDropdownItem, + GlIcon, } from '@gitlab/ui'; import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; import { __ } from '../../locale'; -import Icon from '../../vue_shared/components/icon.vue'; import getRefMixin from '../mixins/get_ref'; import projectShortPathQuery from '../queries/project_short_path.query.graphql'; import projectPathQuery from '../queries/project_path.query.graphql'; @@ -24,7 +24,7 @@ export default { GlDeprecatedDropdownDivider, GlDeprecatedDropdownHeader, GlDeprecatedDropdownItem, - Icon, + GlIcon, }, apollo: { projectShortPath: { @@ -249,8 +249,8 @@ export default { <gl-deprecated-dropdown toggle-class="add-to-tree qa-add-to-tree ml-1"> <template #button-content> <span class="sr-only">{{ __('Add to tree') }}</span> - <icon name="plus" :size="16" class="float-left" /> - <icon name="chevron-down" :size="16" class="float-left" /> + <gl-icon name="plus" :size="16" class="float-left" /> + <gl-icon name="chevron-down" :size="16" class="float-left" /> </template> <template v-for="(item, i) in dropdownItems"> <component :is="getComponent(item.type)" :key="i" v-bind="item.attrs"> diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue index 3337ce6c6df..59831890a4e 100644 --- a/app/assets/javascripts/repository/components/last_commit.vue +++ b/app/assets/javascripts/repository/components/last_commit.vue @@ -1,8 +1,8 @@ <script> -import { GlTooltipDirective, GlLink, GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui'; +/* eslint-disable vue/no-v-html */ +import { GlTooltipDirective, GlLink, GlDeprecatedButton, GlLoadingIcon, GlIcon } from '@gitlab/ui'; import defaultAvatarUrl from 'images/no_avatar.png'; import { sprintf, s__ } from '~/locale'; -import Icon from '../../vue_shared/components/icon.vue'; import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; import CiIcon from '../../vue_shared/components/ci_icon.vue'; @@ -13,7 +13,7 @@ import pathLastCommitQuery from '../queries/path_last_commit.query.graphql'; export default { components: { - Icon, + GlIcon, UserAvatarLink, TimeagoTooltip, ClipboardButton, @@ -130,7 +130,7 @@ export default { class="text-expander" @click="toggleShowDescription" > - <icon name="ellipsis_h" :size="10" /> + <gl-icon name="ellipsis_h" :size="10" /> </gl-deprecated-button> <div class="committer"> <gl-link diff --git a/app/assets/javascripts/repository/components/preview/index.vue b/app/assets/javascripts/repository/components/preview/index.vue index 013092ffefd..eca53f73a7f 100644 --- a/app/assets/javascripts/repository/components/preview/index.vue +++ b/app/assets/javascripts/repository/components/preview/index.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import $ from 'jquery'; import '~/behaviors/markdown/render_gfm'; import { GlLink, GlLoadingIcon } from '@gitlab/ui'; diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue index d0cc617d755..c6652c57c1f 100644 --- a/app/assets/javascripts/repository/components/table/index.vue +++ b/app/assets/javascripts/repository/components/table/index.vue @@ -1,5 +1,5 @@ <script> -import { GlSkeletonLoading } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlButton } from '@gitlab/ui'; import { sprintf, __ } from '../../../locale'; import getRefMixin from '../../mixins/get_ref'; import projectPathQuery from '../../queries/project_path.query.graphql'; @@ -13,6 +13,7 @@ export default { TableHeader, TableRow, ParentRow, + GlButton, }, mixins: [getRefMixin], apollo: { @@ -39,6 +40,10 @@ export default { required: false, default: '', }, + hasMore: { + type: Boolean, + required: true, + }, }, data() { return { @@ -65,6 +70,11 @@ export default { return !this.isLoading && ['', '/'].indexOf(this.path) === -1; }, }, + methods: { + showMore() { + this.$emit('showMore'); + }, + }, }; </script> @@ -110,6 +120,20 @@ export default { <td><gl-skeleton-loading :lines="1" class="ml-auto h-auto w-50" /></td> </tr> </template> + <template v-if="hasMore"> + <tr> + <td align="center" colspan="3" class="gl-p-0!"> + <gl-button + variant="link" + class="gl-display-flex gl-w-full gl-py-4!" + :loading="isLoading" + @click="showMore" + > + {{ s__('ProjectFileTree|Show more') }} + </gl-button> + </td> + </tr> + </template> </tbody> </table> </div> diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index d2fef6693e2..d749a8c0dee 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -1,9 +1,10 @@ <script> +/* eslint-disable vue/no-v-html */ import { escapeRegExp } from 'lodash'; import { GlBadge, GlLink, - GlSkeletonLoading, + GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlTooltipDirective, GlLoadingIcon, GlIcon, diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue index fe3065a2145..365b6cbb550 100644 --- a/app/assets/javascripts/repository/components/tree_content.vue +++ b/app/assets/javascripts/repository/components/tree_content.vue @@ -1,5 +1,4 @@ <script> -import { GlButton } from '@gitlab/ui'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { __ } from '../../locale'; import FileTable from './table/index.vue'; @@ -17,7 +16,6 @@ export default { components: { FileTable, FilePreview, - GlButton, }, mixins: [getRefMixin], apollo: { @@ -127,7 +125,7 @@ export default { .concat(data.trees.pageInfo, data.submodules.pageInfo, data.blobs.pageInfo) .find(({ hasNextPage }) => hasNextPage); }, - showMore() { + handleShowMore() { this.clickedShowMore = true; this.fetchFiles(); }, @@ -142,20 +140,9 @@ export default { :entries="entries" :is-loading="isLoadingFiles" :loading-path="loadingPath" + :has-more="hasShowMore" + @showMore="handleShowMore" /> - <div - v-if="hasShowMore" - class="gl-border-1 gl-border-gray-100 gl-rounded-base gl-border-t-none gl-border-b-solid gl-border-l-solid gl-border-r-solid gl-rounded-top-right-none gl-rounded-top-left-none gl-mt-n1" - > - <gl-button - variant="link" - class="gl-display-flex gl-w-full gl-py-4!" - :loading="isLoadingFiles" - @click="showMore" - > - {{ s__('ProjectFileTree|Show more') }} - </gl-button> - </div> <file-preview v-if="readme" :blob="readme" /> </div> </template> diff --git a/app/assets/javascripts/repository/components/web_ide_link.vue b/app/assets/javascripts/repository/components/web_ide_link.vue deleted file mode 100644 index 6549d5a3878..00000000000 --- a/app/assets/javascripts/repository/components/web_ide_link.vue +++ /dev/null @@ -1,47 +0,0 @@ -<script> -import TreeActionLink from './tree_action_link.vue'; -import { __ } from '~/locale'; -import { webIDEUrl } from '~/lib/utils/url_utility'; - -export default { - components: { - TreeActionLink, - }, - props: { - projectPath: { - type: String, - required: true, - }, - refSha: { - type: String, - required: true, - }, - canPushCode: { - type: Boolean, - required: false, - default: true, - }, - forkPath: { - type: String, - required: false, - default: '', - }, - }, - computed: { - showLinkToFork() { - return !this.canPushCode && this.forkPath; - }, - text() { - return this.showLinkToFork ? __('Edit fork in Web IDE') : __('Web IDE'); - }, - path() { - const path = this.showLinkToFork ? this.forkPath : this.projectPath; - return webIDEUrl(`/${path}/edit/${this.refSha}/-/${this.$route.params.path || ''}`); - }, - }, -}; -</script> - -<template> - <tree-action-link :path="path" :text="text" data-qa-selector="web_ide_button" /> -</template> diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js index 450a1571165..8dd18027945 100644 --- a/app/assets/javascripts/repository/graphql.js +++ b/app/assets/javascripts/repository/graphql.js @@ -58,6 +58,7 @@ const defaultClient = createDefaultClient( /* eslint-enable @gitlab/require-i18n-strings */ }, }, + assumeImmutableResults: true, }, ); diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js index 187bbfed125..7f72524b6fe 100644 --- a/app/assets/javascripts/repository/index.js +++ b/app/assets/javascripts/repository/index.js @@ -1,30 +1,22 @@ import Vue from 'vue'; -import { escapeFileUrl } from '../lib/utils/url_utility'; +import { escapeFileUrl, joinPaths, webIDEUrl } from '../lib/utils/url_utility'; import createRouter from './router'; import App from './components/app.vue'; import Breadcrumbs from './components/breadcrumbs.vue'; import LastCommit from './components/last_commit.vue'; import TreeActionLink from './components/tree_action_link.vue'; -import WebIdeLink from './components/web_ide_link.vue'; +import WebIdeLink from '~/vue_shared/components/web_ide_link.vue'; import DirectoryDownloadLinks from './components/directory_download_links.vue'; import apolloProvider from './graphql'; import { setTitle } from './utils/title'; import { updateFormAction } from './utils/dom'; -import { parseBoolean } from '../lib/utils/common_utils'; +import { convertObjectPropsToCamelCase, parseBoolean } from '../lib/utils/common_utils'; import { __ } from '../locale'; export default function setupVueRepositoryList() { const el = document.getElementById('js-tree-list'); const { dataset } = el; - const { - canPushCode, - projectPath, - projectShortPath, - forkPath, - ref, - escapedRef, - fullName, - } = dataset; + const { projectPath, projectShortPath, ref, escapedRef, fullName } = dataset; const router = createRouter(projectPath, escapedRef); apolloProvider.clients.defaultClient.cache.writeData({ @@ -121,6 +113,10 @@ export default function setupVueRepositoryList() { const webIdeLinkEl = document.getElementById('js-tree-web-ide-link'); if (webIdeLinkEl) { + const { ideBasePath, ...options } = convertObjectPropsToCamelCase( + JSON.parse(webIdeLinkEl.dataset.options), + ); + // eslint-disable-next-line no-new new Vue({ el: webIdeLinkEl, @@ -128,10 +124,10 @@ export default function setupVueRepositoryList() { render(h) { return h(WebIdeLink, { props: { - projectPath, - refSha: ref, - forkPath, - canPushCode: parseBoolean(canPushCode), + webIdeUrl: webIDEUrl( + joinPaths('/', ideBasePath, 'edit', ref, '-', this.$route.params.path || '', '/'), + ), + ...options, }, }); }, diff --git a/app/assets/javascripts/repository/log_tree.js b/app/assets/javascripts/repository/log_tree.js index 704dd88aabe..361e0b62bb7 100644 --- a/app/assets/javascripts/repository/log_tree.js +++ b/app/assets/javascripts/repository/log_tree.js @@ -1,3 +1,4 @@ +import produce from 'immer'; import { normalizeData } from 'ee_else_ce/repository/utils/commit'; import axios from '~/lib/utils/axios_utils'; import commitsQuery from './queries/commits.query.graphql'; @@ -34,16 +35,18 @@ export function fetchLogsTree(client, path, offset, resolver = null) { params: { format: 'json', offset }, }, ) - .then(({ data, headers }) => { + .then(({ data: newData, headers }) => { const headerLogsOffset = headers['more-logs-offset']; - const { commits } = client.readQuery({ query: commitsQuery }); - const newCommitData = [...commits, ...normalizeData(data, path)]; + const sourceData = client.readQuery({ query: commitsQuery }); + const data = produce(sourceData, draftState => { + draftState.commits.push(...normalizeData(newData, path)); + }); client.writeQuery({ query: commitsQuery, - data: { commits: newCommitData }, + data, }); - resolvers.forEach(r => resolveCommit(newCommitData, path, r)); + resolvers.forEach(r => resolveCommit(data.commits, path, r)); fetchpromise = null; diff --git a/app/assets/javascripts/repository/router.js b/app/assets/javascripts/repository/router.js index c5646c32850..38a596e229e 100644 --- a/app/assets/javascripts/repository/router.js +++ b/app/assets/javascripts/repository/router.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import VueRouter from 'vue-router'; +import { escapeRegExp } from 'lodash'; import { joinPaths } from '../lib/utils/url_utility'; import IndexPage from './pages/index.vue'; import TreePage from './pages/tree.vue'; @@ -27,7 +28,7 @@ export default function createRouter(base, baseRef) { { name: 'treePath', // Support without decoding as well just in case the ref doesn't need to be decoded - path: `(/-)?/tree/${baseRef}/:path*`, + path: `(/-)?/tree/${escapeRegExp(baseRef)}/:path*`, ...treePathRoute, }, { diff --git a/app/assets/javascripts/repository/utils/commit.js b/app/assets/javascripts/repository/utils/commit.js index 90ac01c5874..0704ac1627f 100644 --- a/app/assets/javascripts/repository/utils/commit.js +++ b/app/assets/javascripts/repository/utils/commit.js @@ -1,4 +1,3 @@ -// eslint-disable-next-line import/prefer-default-export export function normalizeData(data, path, extra = () => {}) { return data.map(d => ({ sha: d.commit.id, diff --git a/app/assets/javascripts/repository/utils/icon.js b/app/assets/javascripts/repository/utils/icon.js index 661ebb6edfc..47b045c7eaf 100644 --- a/app/assets/javascripts/repository/utils/icon.js +++ b/app/assets/javascripts/repository/utils/icon.js @@ -88,7 +88,6 @@ const fileTypeIcons = [ }, ]; -// eslint-disable-next-line import/prefer-default-export export const getIconName = (type, path) => { if (entryTypeIcons[type]) return entryTypeIcons[type]; diff --git a/app/assets/javascripts/repository/utils/readme.js b/app/assets/javascripts/repository/utils/readme.js index 5b62271b02e..50692779b1a 100644 --- a/app/assets/javascripts/repository/utils/readme.js +++ b/app/assets/javascripts/repository/utils/readme.js @@ -28,5 +28,4 @@ const isPlainReadme = file => { return re.test(file.name); }; -// eslint-disable-next-line import/prefer-default-export export const readmeFile = blobs => blobs.find(isRichReadme) || blobs.find(isPlainReadme); diff --git a/app/assets/javascripts/search/state_filter/components/state_filter.vue b/app/assets/javascripts/search/state_filter/components/state_filter.vue new file mode 100644 index 00000000000..f08adaf8c83 --- /dev/null +++ b/app/assets/javascripts/search/state_filter/components/state_filter.vue @@ -0,0 +1,94 @@ +<script> +import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui'; +import { + FILTER_STATES, + SCOPES, + FILTER_STATES_BY_SCOPE, + FILTER_HEADER, + FILTER_TEXT, +} from '../constants'; +import { setUrlParams, visitUrl } from '~/lib/utils/url_utility'; + +const FILTERS_ARRAY = Object.values(FILTER_STATES); + +export default { + name: 'StateFilter', + components: { + GlDropdown, + GlDropdownItem, + GlDropdownDivider, + }, + props: { + scope: { + type: String, + required: true, + }, + state: { + type: String, + required: false, + default: FILTER_STATES.ANY.value, + validator: v => FILTERS_ARRAY.some(({ value }) => value === v), + }, + }, + computed: { + selectedFilterText() { + const filter = FILTERS_ARRAY.find(({ value }) => value === this.selectedFilter); + if (!filter || filter === FILTER_STATES.ANY) { + return FILTER_TEXT; + } + + return filter.label; + }, + showDropdown() { + return Object.values(SCOPES).includes(this.scope); + }, + selectedFilter: { + get() { + if (FILTERS_ARRAY.some(({ value }) => value === this.state)) { + return this.state; + } + + return FILTER_STATES.ANY.value; + }, + set(state) { + visitUrl(setUrlParams({ state })); + }, + }, + }, + methods: { + dropDownItemClass(filter) { + return { + 'gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2': + filter === FILTER_STATES.ANY, + }; + }, + isFilterSelected(filter) { + return filter === this.selectedFilter; + }, + handleFilterChange(state) { + this.selectedFilter = state; + }, + }, + filterStates: FILTER_STATES, + filterHeader: FILTER_HEADER, + filtersByScope: FILTER_STATES_BY_SCOPE, +}; +</script> + +<template> + <gl-dropdown v-if="showDropdown" :text="selectedFilterText" class="col-sm-3 gl-pt-4 gl-pl-0"> + <header class="gl-text-center gl-font-weight-bold gl-font-lg"> + {{ $options.filterHeader }} + </header> + <gl-dropdown-divider /> + <gl-dropdown-item + v-for="filter in $options.filtersByScope[scope]" + :key="filter.value" + :is-check-item="true" + :is-checked="isFilterSelected(filter.value)" + :class="dropDownItemClass(filter)" + @click="handleFilterChange(filter.value)" + >{{ filter.label }}</gl-dropdown-item + > + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/search/state_filter/constants.js b/app/assets/javascripts/search/state_filter/constants.js new file mode 100644 index 00000000000..2f11cab9044 --- /dev/null +++ b/app/assets/javascripts/search/state_filter/constants.js @@ -0,0 +1,39 @@ +import { __ } from '~/locale'; + +export const FILTER_HEADER = __('Status'); + +export const FILTER_TEXT = __('Any Status'); + +export const FILTER_STATES = { + ANY: { + label: __('Any'), + value: 'all', + }, + OPEN: { + label: __('Open'), + value: 'opened', + }, + CLOSED: { + label: __('Closed'), + value: 'closed', + }, + MERGED: { + label: __('Merged'), + value: 'merged', + }, +}; + +export const SCOPES = { + ISSUES: 'issues', + MERGE_REQUESTS: 'merge_requests', +}; + +export const FILTER_STATES_BY_SCOPE = { + [SCOPES.ISSUES]: [FILTER_STATES.ANY, FILTER_STATES.OPEN, FILTER_STATES.CLOSED], + [SCOPES.MERGE_REQUESTS]: [ + FILTER_STATES.ANY, + FILTER_STATES.OPEN, + FILTER_STATES.MERGED, + FILTER_STATES.CLOSED, + ], +}; diff --git a/app/assets/javascripts/search/state_filter/index.js b/app/assets/javascripts/search/state_filter/index.js new file mode 100644 index 00000000000..13708574cfb --- /dev/null +++ b/app/assets/javascripts/search/state_filter/index.js @@ -0,0 +1,34 @@ +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; +import StateFilter from './components/state_filter.vue'; + +Vue.use(Translate); + +export default () => { + const el = document.getElementById('js-search-filter-by-state'); + + if (!el) return false; + + return new Vue({ + el, + components: { + StateFilter, + }, + data() { + const { dataset } = this.$options.el; + return { + scope: dataset.scope, + state: dataset.state, + }; + }, + + render(createElement) { + return createElement('state-filter', { + props: { + scope: this.scope, + state: this.state, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 990c8faf253..7073b9ca12d 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -13,6 +13,7 @@ import { spriteIcon, } from './lib/utils/common_utils'; import Tracking from '~/tracking'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; /** * Search input in top navigation bar. @@ -119,7 +120,7 @@ export class SearchAutocomplete { } createAutocomplete() { - return this.searchInput.glDropdown({ + return initDeprecatedJQueryDropdown(this.searchInput, { filterInputBlur: false, filterable: true, filterRemote: true, @@ -134,6 +135,7 @@ export class SearchAutocomplete { data: this.getData.bind(this), selectable: true, clicked: this.onClick.bind(this), + trackSuggestionClickedLabel: 'search_autocomplete_suggestion', }); } @@ -145,10 +147,10 @@ export class SearchAutocomplete { if (!term) { const contents = this.getCategoryContents(); if (contents) { - const glDropdownInstance = this.searchInput.data('glDropdown'); + const deprecatedJQueryDropdownInstance = this.searchInput.data('deprecatedJQueryDropdown'); - if (glDropdownInstance) { - glDropdownInstance.filter.options.callback(contents); + if (deprecatedJQueryDropdownInstance) { + deprecatedJQueryDropdownInstance.filter.options.callback(contents); } this.enableAutocomplete(); } @@ -463,7 +465,7 @@ export class SearchAutocomplete { } highlightFirstRow() { - this.searchInput.data('glDropdown').highlightRowAtIndex(null, 0); + this.searchInput.data('deprecatedJQueryDropdown').highlightRowAtIndex(null, 0); } getAvatar(item) { diff --git a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue index adc7d3c5ebd..1ccf5e9e032 100644 --- a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue +++ b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import Vue from 'vue'; import { GlFormGroup, GlDeprecatedButton, GlModal, GlToast, GlToggle } from '@gitlab/ui'; import { mapState, mapActions } from 'vuex'; diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue index c44a14f1785..e15549f5864 100644 --- a/app/assets/javascripts/serverless/components/functions.vue +++ b/app/assets/javascripts/serverless/components/functions.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { mapState, mapActions, mapGetters } from 'vuex'; import { GlLink, GlLoadingIcon } from '@gitlab/ui'; import { sprintf, s__ } from '~/locale'; diff --git a/app/assets/javascripts/set_status_modal/event_hub.js b/app/assets/javascripts/set_status_modal/event_hub.js deleted file mode 100644 index e31806ad199..00000000000 --- a/app/assets/javascripts/set_status_modal/event_hub.js +++ /dev/null @@ -1,3 +0,0 @@ -import createEventHub from '~/helpers/event_hub_factory'; - -export default createEventHub(); diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue b/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue deleted file mode 100644 index 0e8b6d93f42..00000000000 --- a/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue +++ /dev/null @@ -1,27 +0,0 @@ -<script> -import { s__ } from '~/locale'; -import eventHub from './event_hub'; - -export default { - props: { - hasStatus: { - type: Boolean, - required: true, - }, - }, - computed: { - buttonText() { - return this.hasStatus ? s__('SetStatusModal|Edit status') : s__('SetStatusModal|Set status'); - }, - }, - methods: { - openModal() { - eventHub.$emit('openModal'); - }, - }, -}; -</script> - -<template> - <button type="button" class="btn menu-item" @click="openModal">{{ buttonText }}</button> -</template> 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 cb047530c17..09e893ff285 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 @@ -1,12 +1,11 @@ <script> +/* eslint-disable vue/no-v-html */ import $ from 'jquery'; import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete'; -import { GlModal, GlTooltipDirective } from '@gitlab/ui'; +import { GlModal, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { deprecatedCreateFlash as createFlash } from '~/flash'; -import Icon from '~/vue_shared/components/icon.vue'; import { __, s__ } from '~/locale'; import Api from '~/api'; -import eventHub from './event_hub'; import EmojiMenuInModal from './emoji_menu_in_modal'; import * as Emoji from '~/emoji'; @@ -14,7 +13,7 @@ const emojiMenuClass = 'js-modal-status-emoji-menu'; export default { components: { - Icon, + GlIcon, GlModal, }, directives: { @@ -48,15 +47,12 @@ export default { }, }, mounted() { - eventHub.$on('openModal', this.openModal); + this.$root.$emit('bv::show::modal', this.modalId); }, beforeDestroy() { this.emojiMenu.destroy(); }, methods: { - openModal() { - this.$root.$emit('bv::show::modal', this.modalId); - }, closeModal() { this.$root.$emit('bv::hide::modal', this.modalId); }, @@ -196,9 +192,9 @@ export default { v-show="noEmoji" class="js-no-emoji-placeholder no-emoji-placeholder position-relative" > - <icon name="slight-smile" class="award-control-icon-neutral" /> - <icon name="smiley" class="award-control-icon-positive" /> - <icon name="smile" class="award-control-icon-super-positive" /> + <gl-icon name="slight-smile" class="award-control-icon-neutral" /> + <gl-icon name="smiley" class="award-control-icon-positive" /> + <gl-icon name="smile" class="award-control-icon-super-positive" /> </span> </button> </span> @@ -223,7 +219,7 @@ export default { class="js-clear-user-status-button clear-user-status btn" @click="clearStatusInputs()" > - <icon name="close" /> + <gl-icon name="close" /> </button> </span> </div> diff --git a/app/assets/javascripts/shared/milestones/form.js b/app/assets/javascripts/shared/milestones/form.js index 0ff84dc4667..9ee02f923d5 100644 --- a/app/assets/javascripts/shared/milestones/form.js +++ b/app/assets/javascripts/shared/milestones/form.js @@ -16,5 +16,6 @@ export default (initGFM = true) => { milestones: initGFM, labels: initGFM, snippets: initGFM, + vulnerabilities: initGFM, }); }; diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue index 9a60172db2e..878b331fb3c 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue @@ -59,7 +59,7 @@ export default { }; }, assigneeUrl() { - return this.user.web_url; + return this.user.web_url || this.user.webUrl; }, }, }; 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 7375855f899..eabd4d88d52 100644 --- a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue +++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue @@ -1,5 +1,5 @@ <script> -import { GlTooltipDirective } from '@gitlab/ui'; +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import CollapsedAssignee from './collapsed_assignee.vue'; @@ -12,6 +12,7 @@ export default { }, components: { CollapsedAssignee, + GlIcon, }, props: { users: { @@ -102,7 +103,7 @@ export default { :title="tooltipTitle" class="sidebar-collapsed-icon sidebar-collapsed-user" > - <i v-if="hasNoUsers" :aria-label="__('None')" class="fa fa-user"> </i> + <gl-icon v-if="hasNoUsers" name="user" :aria-label="__('None')" /> <collapsed-assignee v-for="user in collapsedUsers" :key="user.id" diff --git a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue new file mode 100644 index 00000000000..4697d85472b --- /dev/null +++ b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue @@ -0,0 +1,37 @@ +<script> +import { n__ } from '~/locale'; +import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_assignee_list.vue'; + +export default { + components: { + UncollapsedAssigneeList, + }, + inject: ['rootPath'], + props: { + users: { + type: Array, + required: true, + }, + }, + computed: { + assigneesText() { + return n__('Assignee', '%d Assignees', this.users.length); + }, + emptyUsers() { + return this.users.length === 0; + }, + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-flex-direction-column"> + <label data-testid="assigneeLabel">{{ assigneesText }}</label> + <div v-if="emptyUsers" data-testid="none"> + <span> + {{ __('None') }} + </span> + </div> + <uncollapsed-assignee-list v-else :users="users" :root-path="rootPath" /> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index 14c14d0bad1..2f714ac3847 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -62,7 +62,7 @@ export default { this.addAssignee = this.store.addAssignee.bind(this.store); this.removeAllAssignees = this.store.removeAllAssignees.bind(this.store); - // Get events from glDropdown + // Get events from deprecatedJQueryDropdown eventHub.$on('sidebar.removeAssignee', this.removeAssignee); eventHub.$on('sidebar.addAssignee', this.addAssignee); eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees); diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue index fed9e5886c0..95934c0ef2a 100644 --- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue +++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue @@ -73,9 +73,9 @@ export default { :root-path="rootPath" :issuable-type="issuableType" > - <div class="ml-2"> - <span class="author"> {{ user.name }} </span> - <span class="username"> {{ username }} </span> + <div class="ml-2 gl-line-height-normal"> + <div>{{ user.name }}</div> + <div>{{ username }}</div> </div> </assignee-avatar-link> <div v-else> diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue index c6f7d5e44ad..2530cb77acd 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -1,18 +1,17 @@ <script> import { mapState } from 'vuex'; +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; -import tooltip from '~/vue_shared/directives/tooltip'; -import Icon from '~/vue_shared/components/icon.vue'; import eventHub from '~/sidebar/event_hub'; import EditForm from './edit_form.vue'; export default { components: { EditForm, - Icon, + GlIcon, }, directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, props: { fullPath: { @@ -73,15 +72,12 @@ export default { <div class="block issuable-sidebar-item confidentiality"> <div ref="collapseIcon" - v-tooltip + v-gl-tooltip.viewport.left :title="tooltipLabel" class="sidebar-collapsed-icon" - data-container="body" - data-placement="left" - data-boundary="viewport" @click="toggleForm" > - <icon :name="confidentialityIcon" aria-hidden="true" /> + <gl-icon :name="confidentialityIcon" aria-hidden="true" /> </div> <div class="title hide-collapsed"> {{ __('Confidentiality') }} @@ -105,11 +101,11 @@ export default { :issuable-type="issuableType" /> <div v-if="!confidential" class="no-value sidebar-item-value" data-testid="not-confidential"> - <icon :size="16" name="eye" aria-hidden="true" class="sidebar-item-icon inline" /> + <gl-icon :size="16" name="eye" aria-hidden="true" class="sidebar-item-icon inline" /> {{ __('Not confidential') }} </div> <div v-else class="value sidebar-item-value hide-collapsed"> - <icon + <gl-icon :size="16" name="eye-slash" aria-hidden="true" diff --git a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue new file mode 100644 index 00000000000..d7be8927c29 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue @@ -0,0 +1,97 @@ +<script> +import $ from 'jquery'; +import { difference, union } from 'lodash'; +import { mapState, mapActions } from 'vuex'; +import flash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; +import { __ } from '~/locale'; +import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants'; +import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue'; + +export default { + components: { + LabelsSelect, + }, + variant: DropdownVariant.Sidebar, + inject: [ + 'allowLabelCreate', + 'allowLabelEdit', + 'allowScopedLabels', + 'iid', + 'initiallySelectedLabels', + 'issuableType', + 'labelsFetchPath', + 'labelsManagePath', + 'labelsUpdatePath', + 'projectIssuesPath', + 'projectPath', + ], + data: () => ({ + labelsSelectInProgress: false, + }), + computed: { + ...mapState(['selectedLabels']), + }, + mounted() { + this.setInitialState({ + selectedLabels: this.initiallySelectedLabels, + }); + }, + methods: { + ...mapActions(['setInitialState', 'replaceSelectedLabels']), + handleDropdownClose() { + $(this.$el).trigger('hidden.gl.dropdown'); + }, + handleUpdateSelectedLabels(labels) { + const currentLabelIds = this.selectedLabels.map(label => label.id); + const userAddedLabelIds = labels.filter(label => label.set).map(label => label.id); + const userRemovedLabelIds = labels.filter(label => !label.set).map(label => label.id); + + const issuableLabels = difference( + union(currentLabelIds, userAddedLabelIds), + userRemovedLabelIds, + ); + + this.labelsSelectInProgress = true; + + axios({ + data: { + [this.issuableType]: { + label_ids: issuableLabels, + }, + }, + method: 'put', + url: this.labelsUpdatePath, + }) + .then(({ data }) => this.replaceSelectedLabels(data.labels)) + .catch(() => flash(__('An error occurred while updating labels.'))) + .finally(() => { + this.labelsSelectInProgress = false; + }); + }, + }, +}; +</script> + +<template> + <labels-select + class="block labels js-labels-block" + :allow-label-create="allowLabelCreate" + :allow-label-edit="allowLabelEdit" + :allow-multiselect="true" + :allow-scoped-labels="allowScopedLabels" + :footer-create-label-title="__('Create project label')" + :footer-manage-label-title="__('Manage project labels')" + :labels-create-title="__('Create project label')" + :labels-fetch-path="labelsFetchPath" + :labels-filter-base-path="projectIssuesPath" + :labels-manage-path="labelsManagePath" + :labels-select-in-progress="labelsSelectInProgress" + :selected-labels="selectedLabels" + :variant="$options.sidebar" + @onDropdownClose="handleDropdownClose" + @updateSelectedLabels="handleUpdateSelectedLabels" + > + {{ __('None') }} + </labels-select> +</template> diff --git a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue index 1b4968fabf6..53ee7f46ad9 100644 --- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue +++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue @@ -1,8 +1,8 @@ <script> import { mapGetters } from 'vuex'; +import { GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import tooltip from '~/vue_shared/directives/tooltip'; -import Icon from '~/vue_shared/components/icon.vue'; import eventHub from '~/sidebar/event_hub'; import editForm from './edit_form.vue'; @@ -22,7 +22,7 @@ export default { }, components: { editForm, - Icon, + GlIcon, }, directives: { @@ -88,7 +88,7 @@ export default { data-boundary="viewport" @click="toggleForm" > - <icon :name="lockStatus.icon" class="sidebar-item-icon is-active" /> + <gl-icon :name="lockStatus.icon" class="sidebar-item-icon is-active" /> </div> <div class="title hide-collapsed"> @@ -116,7 +116,7 @@ export default { /> <div data-testid="lock-status" class="sidebar-item-value" :class="lockStatus.class"> - <icon + <gl-icon :size="16" :name="lockStatus.icon" class="sidebar-item-icon" diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue index d2904f4157c..e7dbc47aea1 100644 --- a/app/assets/javascripts/sidebar/components/participants/participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/participants.vue @@ -1,5 +1,5 @@ <script> -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlIcon, GlLoadingIcon } from '@gitlab/ui'; import { __, n__, sprintf } from '~/locale'; import tooltip from '~/vue_shared/directives/tooltip'; import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; @@ -10,6 +10,7 @@ export default { }, components: { userAvatarImage, + GlIcon, GlLoadingIcon, }, props: { @@ -94,7 +95,7 @@ export default { data-boundary="viewport" @click="onClickCollapsedIcon" > - <i class="fa fa-users" aria-hidden="true"> </i> + <gl-icon name="users" /> <gl-loading-icon v-if="loading" /> <span v-else data-testid="collapsed-count"> {{ participantCount }} </span> </div> diff --git a/app/assets/javascripts/sidebar/components/severity/constants.js b/app/assets/javascripts/sidebar/components/severity/constants.js new file mode 100644 index 00000000000..4f58ff38121 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/severity/constants.js @@ -0,0 +1,41 @@ +import { __, s__ } from '~/locale'; + +export const INCIDENT_SEVERITY = { + CRITICAL: { + value: 'CRITICAL', + icon: 'critical', + label: s__('IncidentManagement|Critical - S1'), + }, + HIGH: { + value: 'HIGH', + icon: 'high', + label: s__('IncidentManagement|High - S2'), + }, + MEDIUM: { + value: 'MEDIUM', + icon: 'medium', + label: s__('IncidentManagement|Medium - S3'), + }, + LOW: { + value: 'LOW', + icon: 'low', + label: s__('IncidentManagement|Low - S4'), + }, + UNKNOWN: { + value: 'UNKNOWN', + icon: 'unknown', + label: s__('IncidentManagement|Unknown'), + }, +}; + +export const ISSUABLE_TYPES = { + INCIDENT: 'incident', +}; + +export const I18N = { + UPDATE_SEVERITY_ERROR: s__('SeverityWidget|There was an error while updating severity.'), + TRY_AGAIN: __('Please try again'), + EDIT: __('Edit'), + SEVERITY: s__('SeverityWidget|Severity'), + SEVERITY_VALUE: s__('SeverityWidget|Severity: %{severity}'), +}; diff --git a/app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql b/app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql new file mode 100644 index 00000000000..750e757971f --- /dev/null +++ b/app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql @@ -0,0 +1,9 @@ +mutation updateIssuableSeverity($projectPath: ID!, $severity: IssuableSeverity!, $iid: String!) { + issueSetSeverity(input: { iid: $iid, severity: $severity, projectPath: $projectPath }) { + errors + issue { + iid + severity + } + } +} diff --git a/app/assets/javascripts/sidebar/components/severity/severity.vue b/app/assets/javascripts/sidebar/components/severity/severity.vue new file mode 100644 index 00000000000..7e7d62256c9 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/severity/severity.vue @@ -0,0 +1,42 @@ +<script> +import { GlIcon } from '@gitlab/ui'; + +export default { + components: { + GlIcon, + }, + props: { + severity: { + type: Object, + required: true, + validator(severity) { + const { value, label, icon } = severity; + return value && label && icon; + }, + }, + iconSize: { + type: Number, + required: false, + default: 12, + }, + iconOnly: { + type: Boolean, + required: false, + default: false, + }, + }, +}; +</script> + +<template> + <div + class="incident-severity gl-display-inline-flex gl-align-items-center gl-justify-content-between" + > + <gl-icon + :size="iconSize" + :name="`severity-${severity.icon}`" + :class="[`icon-${severity.icon}`, { 'gl-mr-3': !iconOnly }]" + /> + <span v-if="!iconOnly">{{ severity.label }}</span> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue new file mode 100644 index 00000000000..8f3610b912a --- /dev/null +++ b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue @@ -0,0 +1,187 @@ +<script> +import { + GlDropdown, + GlDropdownItem, + GlLoadingIcon, + GlTooltip, + GlSprintf, + GlLink, +} from '@gitlab/ui'; +import { INCIDENT_SEVERITY, ISSUABLE_TYPES, I18N } from './constants'; +import updateIssuableSeverity from './graphql/mutations/update_issuable_severity.mutation.graphql'; +import SeverityToken from './severity.vue'; +import createFlash from '~/flash'; + +export default { + i18n: I18N, + components: { + GlLoadingIcon, + GlTooltip, + GlSprintf, + GlDropdown, + GlDropdownItem, + GlLink, + SeverityToken, + }, + props: { + projectPath: { + type: String, + required: true, + }, + iid: { + type: String, + required: true, + }, + initialSeverity: { + type: String, + required: false, + default: INCIDENT_SEVERITY.UNKNOWN.value, + }, + issuableType: { + type: String, + required: false, + default: ISSUABLE_TYPES.INCIDENT, + validator: value => { + // currently severity is supported only for incidents, but this list might be extended + return [ISSUABLE_TYPES.INCIDENT].includes(value); + }, + }, + }, + data() { + return { + isDropdownShowing: false, + isUpdating: false, + severity: this.initialSeverity, + }; + }, + computed: { + severitiesList() { + switch (this.issuableType) { + case ISSUABLE_TYPES.INCIDENT: + return Object.values(INCIDENT_SEVERITY); + default: + return []; + } + }, + dropdownClass() { + return this.isDropdownShowing ? 'show' : 'gl-display-none'; + }, + selectedItem() { + return this.severitiesList.find(severity => severity.value === this.severity); + }, + }, + mounted() { + document.addEventListener('click', this.handleOffClick); + }, + beforeDestroy() { + document.removeEventListener('click', this.handleOffClick); + }, + methods: { + handleOffClick(event) { + if (!this.isDropdownShowing) { + return; + } + + if (!this.$refs.sidebarSeverity.contains(event.target)) { + this.hideDropdown(); + } + }, + hideDropdown() { + this.isDropdownShowing = false; + const event = new Event('hidden.gl.dropdown'); + this.$el.dispatchEvent(event); + }, + toggleFormDropdown() { + this.isDropdownShowing = !this.isDropdownShowing; + }, + updateSeverity(value) { + this.hideDropdown(); + this.isUpdating = true; + this.$apollo + .mutate({ + mutation: updateIssuableSeverity, + variables: { + iid: this.iid, + severity: value, + projectPath: this.projectPath, + }, + }) + .then(resp => { + const { + data: { + issueSetSeverity: { + errors = [], + issue: { severity }, + }, + }, + } = resp; + + if (errors[0]) { + throw errors[0]; + } + this.severity = severity; + }) + .catch(() => + createFlash({ + message: `${this.$options.i18n.UPDATE_SEVERITY_ERROR} ${this.$options.i18n.TRY_AGAIN}`, + }), + ) + .finally(() => { + this.isUpdating = false; + }); + }, + }, +}; +</script> + +<template> + <div ref="sidebarSeverity" class="block"> + <div ref="severity" class="sidebar-collapsed-icon" @click="toggleFormDropdown"> + <severity-token :severity="selectedItem" :icon-size="14" :icon-only="true" /> + <gl-tooltip :target="() => $refs.severity" boundary="viewport" placement="left"> + <gl-sprintf :message="$options.i18n.SEVERITY_VALUE"> + <template #severity> + {{ selectedItem.label }} + </template> + </gl-sprintf> + </gl-tooltip> + </div> + + <div class="hide-collapsed"> + <p class="title gl-display-flex gl-justify-content-space-between"> + {{ $options.i18n.SEVERITY }} + <gl-link + data-testid="editButton" + href="#" + @click="toggleFormDropdown" + @keydown.esc="hideDropdown" + > + {{ $options.i18n.EDIT }} + </gl-link> + </p> + + <gl-dropdown + :class="dropdownClass" + block + :text="selectedItem.label" + toggle-class="dropdown-menu-toggle gl-mb-2" + @keydown.esc.native="hideDropdown" + > + <gl-dropdown-item + v-for="option in severitiesList" + :key="option.value" + data-testid="severityDropdownItem" + :is-check-item="true" + :is-checked="option.value === severity" + @click="updateSeverity(option.value)" + > + <severity-token :severity="option" /> + </gl-dropdown-item> + </gl-dropdown> + + <gl-loading-icon v-if="isUpdating" :inline="true" /> + + <severity-token v-else-if="!isDropdownShowing" :severity="selectedItem" /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue index 3b92ead8859..0457aad8795 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue @@ -1,7 +1,7 @@ <script> +import { GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import Tracking from '~/tracking'; -import icon from '~/vue_shared/components/icon.vue'; import toggleButton from '~/vue_shared/components/toggle_button.vue'; import tooltip from '~/vue_shared/directives/tooltip'; import eventHub from '../../event_hub'; @@ -16,7 +16,7 @@ export default { tooltip, }, components: { - icon, + GlIcon, toggleButton, }, mixins: [Tracking.mixin({ label: 'right_sidebar' })], @@ -118,7 +118,7 @@ export default { data-boundary="viewport" @click="onClickCollapsedIcon" > - <icon + <gl-icon :name="notificationIcon" :size="16" aria-hidden="true" diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue index 65ecd5be05d..bc2319c0f36 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue @@ -1,12 +1,12 @@ <script> +import { GlIcon } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; -import icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; export default { name: 'TimeTrackingCollapsedState', components: { - icon, + GlIcon, }, directives: { tooltip, @@ -105,7 +105,7 @@ export default { data-placement="left" data-boundary="viewport" > - <icon name="timer" /> + <gl-icon name="timer" /> <div class="time-tracking-collapsed-summary"> <div :class="divClass"> <span :class="spanClass"> {{ text }} </span> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue index 67abde0c22a..b45746e789d 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { sprintf, s__ } from '../../../locale'; import { joinPaths } from '~/lib/utils/url_utility'; diff --git a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue index c2f30310e2e..b2b3b289c5c 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { sprintf, s__ } from '~/locale'; export default { 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 67a8f11b760..a2fb0ebcbc6 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue @@ -106,7 +106,7 @@ export default { <div class="title hide-collapsed"> {{ __('Time tracking') }} <div v-if="!showHelpState" class="help-button float-right" @click="toggleHelpState(true)"> - <i class="fa fa-question-circle" aria-hidden="true"> </i> + <gl-icon name="question-o" /> </div> <div v-if="showHelpState" diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue index 5281c03ab3f..51719df313f 100644 --- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue +++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue @@ -1,10 +1,8 @@ <script> -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import tooltip from '~/vue_shared/directives/tooltip'; -import Icon from '~/vue_shared/components/icon.vue'; - const MARK_TEXT = __('Mark as done'); const TODO_TEXT = __('Add a To-Do'); @@ -13,7 +11,7 @@ export default { tooltip, }, components: { - Icon, + GlIcon, GlLoadingIcon, }, props: { @@ -85,7 +83,7 @@ export default { data-boundary="viewport" @click="handleButtonClick" > - <icon + <gl-icon v-show="collapsedButtonIconVisible" :class="collapsedButtonIconClasses" :name="collapsedButtonIcon" diff --git a/app/assets/javascripts/sidebar/event_hub.js b/app/assets/javascripts/sidebar/event_hub.js index f35506fd5de..dd4bd9a5ab7 100644 --- a/app/assets/javascripts/sidebar/event_hub.js +++ b/app/assets/javascripts/sidebar/event_hub.js @@ -1,6 +1,6 @@ -import Vue from 'vue'; +import createEventHub from '~/helpers/event_hub_factory'; -const eventHub = new Vue(); +const eventHub = createEventHub(); // TODO: remove eventHub hack after code splitting refactor window.emitSidebarEvent = (...args) => eventHub.$emit(...args); diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js index 0fb9cf22653..edeb1bba020 100644 --- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js +++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js @@ -1,7 +1,7 @@ import $ from 'jquery'; -import '~/gl_dropdown'; import { escape } from 'lodash'; import { __ } from '~/locale'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; function isValidProjectId(id) { return id > 0; @@ -27,7 +27,7 @@ class SidebarMoveIssue { } initDropdown() { - this.$dropdownToggle.glDropdown({ + initDeprecatedJQueryDropdown(this.$dropdownToggle, { search: { fields: ['name_with_namespace'], }, diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 015219200db..be559b16420 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -1,21 +1,26 @@ import $ from 'jquery'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import Vuex from 'vuex'; import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue'; import SidebarAssignees from './components/assignees/sidebar_assignees.vue'; +import SidebarLabels from './components/labels/sidebar_labels.vue'; import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue'; import SidebarMoveIssue from './lib/sidebar_move_issue'; import IssuableLockForm from './components/lock/issuable_lock_form.vue'; import sidebarParticipants from './components/participants/sidebar_participants.vue'; import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue'; +import SidebarSeverity from './components/severity/sidebar_severity.vue'; import Translate from '../vue_shared/translate'; import createDefaultClient from '~/lib/graphql'; import { store } from '~/notes/stores'; -import { isInIssuePage } from '~/lib/utils/common_utils'; +import { isInIssuePage, parseBoolean } from '~/lib/utils/common_utils'; import mergeRequestStore from '~/mr_notes/stores'; +import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store'; Vue.use(Translate); Vue.use(VueApollo); +Vue.use(Vuex); function getSidebarOptions() { return JSON.parse(document.querySelector('.js-sidebar-options').innerHTML); @@ -51,6 +56,29 @@ function mountAssigneesComponent(mediator) { }); } +export function mountSidebarLabels() { + const el = document.querySelector('.js-sidebar-labels'); + + if (!el) { + return false; + } + + const labelsStore = new Vuex.Store(labelsSelectModule()); + + return new Vue({ + el, + provide: { + ...el.dataset, + allowLabelCreate: parseBoolean(el.dataset.allowLabelCreate), + allowLabelEdit: parseBoolean(el.dataset.canEdit), + allowScopedLabels: parseBoolean(el.dataset.allowScopedLabels), + initiallySelectedLabels: JSON.parse(el.dataset.selectedLabels), + }, + store: labelsStore, + render: createElement => createElement(SidebarLabels), + }); +} + function mountConfidentialComponent(mediator) { const el = document.getElementById('js-confidential-entry-point'); @@ -159,6 +187,35 @@ function mountTimeTrackingComponent() { }); } +function mountSeverityComponent() { + const severityContainerEl = document.querySelector('#js-severity'); + + if (!severityContainerEl) { + return false; + } + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + const { fullPath, iid, severity } = getSidebarOptions(); + + return new Vue({ + el: severityContainerEl, + apolloProvider, + components: { + SidebarSeverity, + }, + render: createElement => + createElement('sidebar-severity', { + props: { + projectPath: fullPath, + iid: String(iid), + initialSeverity: severity.toUpperCase(), + }, + }), + }); +} + export function mountSidebar(mediator) { mountAssigneesComponent(mediator); mountConfidentialComponent(mediator); @@ -173,6 +230,8 @@ export function mountSidebar(mediator) { ).init(); mountTimeTrackingComponent(); + + mountSeverityComponent(); } export { getSidebarOptions }; diff --git a/app/assets/javascripts/sidebar/queries/sidebarDetailsForHealthStatusFeatureFlag.query.graphql b/app/assets/javascripts/sidebar/queries/sidebarDetailsForHealthStatusFeatureFlag.query.graphql deleted file mode 100644 index 2aff7da4605..00000000000 --- a/app/assets/javascripts/sidebar/queries/sidebarDetailsForHealthStatusFeatureFlag.query.graphql +++ /dev/null @@ -1,7 +0,0 @@ -query($fullPath: ID!, $iid: String!) { - project(fullPath: $fullPath) { - issue(iid: $iid) { - iid - } - } -} diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js index 8714bea1729..a61af631661 100644 --- a/app/assets/javascripts/sidebar/services/sidebar_service.js +++ b/app/assets/javascripts/sidebar/services/sidebar_service.js @@ -1,5 +1,4 @@ import sidebarDetailsQuery from 'ee_else_ce/sidebar/queries/sidebarDetails.query.graphql'; -import sidebarDetailsForHealthStatusFeatureFlagQuery from 'ee_else_ce/sidebar/queries/sidebarDetailsForHealthStatusFeatureFlag.query.graphql'; import axios from '~/lib/utils/axios_utils'; import createGqClient, { fetchPolicies } from '~/lib/graphql'; @@ -27,14 +26,10 @@ export default class SidebarService { } get() { - const hasHealthStatusFeatureFlag = gon.features && gon.features.saveIssuableHealthStatus; - return Promise.all([ axios.get(this.endpoint), gqClient.query({ - query: hasHealthStatusFeatureFlag - ? sidebarDetailsForHealthStatusFeatureFlagQuery - : sidebarDetailsQuery, + query: sidebarDetailsQuery, variables: { fullPath: this.fullPath, iid: this.iid.toString(), diff --git a/app/assets/javascripts/snippet/snippet_edit.js b/app/assets/javascripts/snippet/snippet_edit.js index b0d373b1a4b..3dc74922a77 100644 --- a/app/assets/javascripts/snippet/snippet_edit.js +++ b/app/assets/javascripts/snippet/snippet_edit.js @@ -14,6 +14,7 @@ document.addEventListener('DOMContentLoaded', () => { milestones: false, labels: false, snippets: false, + vulnerabilities: false, }; const projectSnippetOptions = {}; diff --git a/app/assets/javascripts/snippet/snippet_show.js b/app/assets/javascripts/snippet/snippet_show.js index 9a463b4762b..bbddfc579c5 100644 --- a/app/assets/javascripts/snippet/snippet_show.js +++ b/app/assets/javascripts/snippet/snippet_show.js @@ -4,6 +4,7 @@ import ZenMode from '~/zen_mode'; import initNotes from '~/init_notes'; import snippetEmbed from '~/snippet/snippet_embed'; import { SnippetShowInit } from '~/snippets'; +import loadAwardsHandler from '~/awards_handler'; document.addEventListener('DOMContentLoaded', () => { if (!gon.features.snippetsVue) { @@ -16,4 +17,5 @@ document.addEventListener('DOMContentLoaded', () => { SnippetShowInit(); initNotes(); } + loadAwardsHandler(); }); diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue index 0978fcc7f93..1a539aa0876 100644 --- a/app/assets/javascripts/snippets/components/edit.vue +++ b/app/assets/javascripts/snippets/components/edit.vue @@ -4,21 +4,23 @@ import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import { deprecatedCreateFlash as Flash } from '~/flash'; import { __, sprintf } from '~/locale'; import TitleField from '~/vue_shared/components/form/title.vue'; -import { redirectTo } from '~/lib/utils/url_utility'; +import { redirectTo, joinPaths } from '~/lib/utils/url_utility'; import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue'; +import { SNIPPET_MARK_EDIT_APP_START } from '~/performance_constants'; import UpdateSnippetMutation from '../mutations/updateSnippet.mutation.graphql'; import CreateSnippetMutation from '../mutations/createSnippet.mutation.graphql'; import { getSnippetMixin } from '../mixins/snippets'; import { - SNIPPET_VISIBILITY_PRIVATE, SNIPPET_CREATE_MUTATION_ERROR, SNIPPET_UPDATE_MUTATION_ERROR, + SNIPPET_VISIBILITY_PRIVATE, } from '../constants'; +import defaultVisibilityQuery from '../queries/snippet_visibility.query.graphql'; + import SnippetBlobActionsEdit from './snippet_blob_actions_edit.vue'; import SnippetVisibilityEdit from './snippet_visibility_edit.vue'; import SnippetDescriptionEdit from './snippet_description_edit.vue'; -import { SNIPPET_MARK_EDIT_APP_START } from '~/performance_constants'; export default { components: { @@ -31,6 +33,15 @@ export default { GlLoadingIcon, }, mixins: [getSnippetMixin], + apollo: { + defaultVisibility: { + query: defaultVisibilityQuery, + manual: true, + result({ data: { selectedLevel } }) { + this.selectedLevelDefault = selectedLevel; + }, + }, + }, props: { markdownPreviewPath: { type: String, @@ -56,6 +67,7 @@ export default { isUpdating: false, newSnippet: false, actions: [], + selectedLevelDefault: SNIPPET_VISIBILITY_PRIVATE, }; }, computed: { @@ -88,7 +100,7 @@ export default { }, cancelButtonHref() { if (this.newSnippet) { - return this.projectPath ? `/${this.projectPath}/-/snippets` : `/-/snippets`; + return joinPaths('/', gon.relative_url_root, this.projectPath, '-/snippets'); } return this.snippet.webUrl; }, @@ -98,6 +110,13 @@ export default { descriptionFieldId() { return `${this.isProjectSnippet ? 'project' : 'personal'}_snippet_description`; }, + newSnippetSchema() { + return { + title: '', + description: '', + visibilityLevel: this.selectedLevelDefault, + }; + }, }, beforeCreate() { performance.mark(SNIPPET_MARK_EDIT_APP_START); @@ -126,7 +145,7 @@ export default { }, onNewSnippetFetched() { this.newSnippet = true; - this.snippet = this.$options.newSnippetSchema; + this.snippet = this.newSnippetSchema; }, onExistingSnippetFetched() { this.newSnippet = false; @@ -184,11 +203,6 @@ export default { this.actions = actions; }, }, - newSnippetSchema: { - title: '', - description: '', - visibilityLevel: SNIPPET_VISIBILITY_PRIVATE, - }, }; </script> <template> @@ -202,7 +216,7 @@ export default { v-if="isLoading" :label="__('Loading snippet')" size="lg" - class="loading-animation prepend-top-20 append-bottom-20" + class="loading-animation prepend-top-20 gl-mb-6" /> <template v-else> <title-field diff --git a/app/assets/javascripts/snippets/components/embed_dropdown.vue b/app/assets/javascripts/snippets/components/embed_dropdown.vue new file mode 100644 index 00000000000..589754a8b19 --- /dev/null +++ b/app/assets/javascripts/snippets/components/embed_dropdown.vue @@ -0,0 +1,78 @@ +<script> +import { escape as esc } from 'lodash'; +import { + GlButton, + GlDropdown, + GlDropdownSectionHeader, + GlDropdownText, + GlFormInputGroup, + GlTooltipDirective, +} from '@gitlab/ui'; +import { __ } from '~/locale'; + +const MSG_EMBED = __('Embed'); +const MSG_SHARE = __('Share'); +const MSG_COPY = __('Copy'); + +export default { + components: { + GlButton, + GlDropdown, + GlDropdownSectionHeader, + GlDropdownText, + GlFormInputGroup, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + url: { + type: String, + required: true, + }, + }, + computed: { + sections() { + return [ + // eslint-disable-next-line no-useless-escape + { name: MSG_EMBED, value: `<script src="${esc(this.url)}.js"><\/script>` }, + { name: MSG_SHARE, value: this.url }, + ]; + }, + }, + MSG_EMBED, + MSG_COPY, +}; +</script> +<template> + <gl-dropdown + right + :text="$options.MSG_EMBED" + menu-class="gl-px-1! gl-pb-5! gl-dropdown-menu-wide" + > + <template v-for="{ name, value } in sections"> + <gl-dropdown-section-header :key="`header_${name}`" data-testid="header">{{ + name + }}</gl-dropdown-section-header> + <gl-dropdown-text + :key="`input_${name}`" + tag="div" + class="gl-dropdown-text-py-0 gl-dropdown-text-block" + data-testid="input" + > + <gl-form-input-group :value="value" readonly select-on-click> + <template #append> + <gl-button + v-gl-tooltip.hover + :title="$options.MSG_COPY" + :data-clipboard-text="value" + icon="copy-to-clipboard" + data-qa-selector="copy_button" + :data-qa-action="name" + /> + </template> + </gl-form-input-group> + </gl-dropdown-text> + </template> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/snippets/components/show.vue b/app/assets/javascripts/snippets/components/show.vue index ca41fd0a2b1..43be2cb7ed8 100644 --- a/app/assets/javascripts/snippets/components/show.vue +++ b/app/assets/javascripts/snippets/components/show.vue @@ -1,6 +1,6 @@ <script> import { GlLoadingIcon } from '@gitlab/ui'; -import BlobEmbeddable from '~/blob/components/blob_embeddable.vue'; +import EmbedDropdown from './embed_dropdown.vue'; import SnippetHeader from './snippet_header.vue'; import SnippetTitle from './snippet_title.vue'; import SnippetBlob from './snippet_blob_view.vue'; @@ -13,7 +13,7 @@ import { SNIPPET_MARK_VIEW_APP_START } from '~/performance_constants'; export default { components: { - BlobEmbeddable, + EmbedDropdown, SnippetHeader, SnippetTitle, GlLoadingIcon, @@ -40,13 +40,17 @@ export default { v-if="isLoading" :label="__('Loading snippet')" size="lg" - class="loading-animation prepend-top-20 append-bottom-20" + class="loading-animation prepend-top-20 gl-mb-6" /> <template v-else> <snippet-header :snippet="snippet" /> <snippet-title :snippet="snippet" /> <div class="gl-display-flex gl-justify-content-end gl-mb-5"> - <blob-embeddable v-if="embeddable" class="gl-flex-fill-1" :url="snippet.webUrl" /> + <embed-dropdown + v-if="embeddable" + :url="snippet.webUrl" + data-qa-selector="snippet_embed_dropdown" + /> <clone-dropdown-button v-if="canBeCloned" class="gl-ml-3" diff --git a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue index ff03432f942..f3f894ed649 100644 --- a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue +++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue @@ -53,7 +53,10 @@ export default { const url = joinPaths(baseUrl, this.blob.rawPath); axios - .get(url) + .get(url, { + // This prevents axios from automatically JSON.parse response + transformResponse: [f => f], + }) .then(res => { this.notifyAboutUpdates({ content: res.data }); }) @@ -80,7 +83,7 @@ export default { v-if="!blob.isLoaded" :label="__('Loading snippet')" size="lg" - class="loading-animation prepend-top-20 append-bottom-20" + class="loading-animation prepend-top-20 gl-mb-6" /> <blob-content-edit v-else diff --git a/app/assets/javascripts/snippets/components/snippet_description_view.vue b/app/assets/javascripts/snippets/components/snippet_description_view.vue index a5107f09fc7..e462f20535b 100644 --- a/app/assets/javascripts/snippets/components/snippet_description_view.vue +++ b/app/assets/javascripts/snippets/components/snippet_description_view.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue'; export default { diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue index ed087dcfaf9..0ca69f3161a 100644 --- a/app/assets/javascripts/snippets/components/snippet_header.vue +++ b/app/assets/javascripts/snippets/components/snippet_header.vue @@ -17,6 +17,7 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import DeleteSnippetMutation from '../mutations/deleteSnippet.mutation.graphql'; import CanCreatePersonalSnippet from '../queries/userPermissions.query.graphql'; import CanCreateProjectSnippet from '../queries/projectPermissions.query.graphql'; +import { joinPaths } from '~/lib/utils/url_utility'; export default { components: { @@ -96,8 +97,8 @@ export default { condition: this.canCreateSnippet, text: __('New snippet'), href: this.snippet.project - ? `${this.snippet.project.webUrl}/-/snippets/new` - : '/-/snippets/new', + ? joinPaths(this.snippet.project.webUrl, '-/snippets/new') + : joinPaths('/', gon.relative_url_root, '/-/snippets/new'), variant: 'success', category: 'secondary', cssClass: 'ml-2', @@ -137,7 +138,7 @@ export default { redirectToSnippets() { window.location.pathname = this.snippet.project ? `${this.snippet.project.fullPath}/-/snippets` - : 'dashboard/snippets'; + : `${gon.relative_url_root}dashboard/snippets`; }, closeDeleteModal() { this.$refs.deleteModal.hide(); diff --git a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue index 299bb8fcfad..25ad7c214b2 100644 --- a/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue +++ b/app/assets/javascripts/snippets/components/snippet_visibility_edit.vue @@ -1,11 +1,8 @@ <script> import { GlIcon, GlFormGroup, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui'; -import { - SNIPPET_VISIBILITY, - SNIPPET_VISIBILITY_PRIVATE, - SNIPPET_VISIBILITY_INTERNAL, - SNIPPET_VISIBILITY_PUBLIC, -} from '~/snippets/constants'; +import defaultVisibilityQuery from '../queries/snippet_visibility.query.graphql'; +import { defaultSnippetVisibilityLevels } from '../utils/blob'; +import { SNIPPET_LEVELS_RESTRICTED, SNIPPET_LEVELS_DISABLED } from '~/snippets/constants'; export default { components: { @@ -15,6 +12,16 @@ export default { GlFormRadioGroup, GlLink, }, + apollo: { + defaultVisibility: { + query: defaultVisibilityQuery, + manual: true, + result({ data: { visibilityLevels, multipleLevelsRestricted } }) { + this.visibilityLevels = defaultSnippetVisibilityLevels(visibilityLevels); + this.multipleLevelsRestricted = multipleLevelsRestricted; + }, + }, + }, props: { helpLink: { type: String, @@ -28,19 +35,17 @@ export default { }, value: { type: String, - required: false, - default: SNIPPET_VISIBILITY_PRIVATE, + required: true, }, }, - computed: { - visibilityOptions() { - return [ - SNIPPET_VISIBILITY_PRIVATE, - SNIPPET_VISIBILITY_INTERNAL, - SNIPPET_VISIBILITY_PUBLIC, - ].map(key => ({ value: key, ...SNIPPET_VISIBILITY[key] })); - }, + data() { + return { + visibilityLevels: [], + multipleLevelsRestricted: false, + }; }, + SNIPPET_LEVELS_DISABLED, + SNIPPET_LEVELS_RESTRICTED, }; </script> <template> @@ -51,10 +56,10 @@ export default { ><gl-icon :size="12" name="question" /></gl-link> </label> - <gl-form-group id="visibility-level-setting"> - <gl-form-radio-group v-bind="$attrs" :checked="value" stacked v-on="$listeners"> + <gl-form-group id="visibility-level-setting" class="gl-mb-0"> + <gl-form-radio-group :checked="value" stacked v-bind="$attrs" v-on="$listeners"> <gl-form-radio - v-for="option in visibilityOptions" + v-for="option in visibilityLevels" :key="option.value" :value="option.value" class="mb-3" @@ -71,5 +76,12 @@ export default { </gl-form-radio> </gl-form-radio-group> </gl-form-group> + + <div class="text-muted" data-testid="restricted-levels-info"> + <template v-if="!visibilityLevels.length">{{ $options.SNIPPET_LEVELS_DISABLED }}</template> + <template v-else-if="multipleLevelsRestricted">{{ + $options.SNIPPET_LEVELS_RESTRICTED + }}</template> + </div> </div> </template> diff --git a/app/assets/javascripts/snippets/constants.js b/app/assets/javascripts/snippets/constants.js index 12b83525bf7..e75922df15f 100644 --- a/app/assets/javascripts/snippets/constants.js +++ b/app/assets/javascripts/snippets/constants.js @@ -33,3 +33,15 @@ export const SNIPPET_BLOB_ACTION_MOVE = 'move'; export const SNIPPET_BLOB_ACTION_DELETE = 'delete'; export const SNIPPET_MAX_BLOBS = 10; + +export const SNIPPET_LEVELS_MAP = { + 0: SNIPPET_VISIBILITY_PRIVATE, + 10: SNIPPET_VISIBILITY_INTERNAL, + 20: SNIPPET_VISIBILITY_PUBLIC, +}; +export const SNIPPET_LEVELS_RESTRICTED = __( + 'Other visibility settings have been disabled by the administrator.', +); +export const SNIPPET_LEVELS_DISABLED = __( + 'Visibility settings have been disabled by the administrator.', +); diff --git a/app/assets/javascripts/snippets/index.js b/app/assets/javascripts/snippets/index.js index bb5e7d6e3f0..c70ad9b95f8 100644 --- a/app/assets/javascripts/snippets/index.js +++ b/app/assets/javascripts/snippets/index.js @@ -5,6 +5,7 @@ import createDefaultClient from '~/lib/graphql'; import SnippetsShow from './components/show.vue'; import SnippetsEdit from './components/edit.vue'; +import { SNIPPET_LEVELS_MAP, SNIPPET_VISIBILITY_PRIVATE } from '~/snippets/constants'; Vue.use(VueApollo); Vue.use(Translate); @@ -18,13 +19,28 @@ function appFactory(el, Component) { defaultClient: createDefaultClient(), }); + const { + visibilityLevels = '[]', + selectedLevel, + multipleLevelsRestricted, + ...restDataset + } = el.dataset; + + apolloProvider.clients.defaultClient.cache.writeData({ + data: { + visibilityLevels: JSON.parse(visibilityLevels), + selectedLevel: SNIPPET_LEVELS_MAP[selectedLevel] ?? SNIPPET_VISIBILITY_PRIVATE, + multipleLevelsRestricted: 'multipleLevelsRestricted' in el.dataset, + }, + }); + return new Vue({ el, apolloProvider, render(createElement) { return createElement(Component, { props: { - ...el.dataset, + ...restDataset, }, }); }, diff --git a/app/assets/javascripts/snippets/mixins/snippets.js b/app/assets/javascripts/snippets/mixins/snippets.js index 3f5d64a768f..15daaa8d84a 100644 --- a/app/assets/javascripts/snippets/mixins/snippets.js +++ b/app/assets/javascripts/snippets/mixins/snippets.js @@ -2,7 +2,6 @@ import GetSnippetQuery from '../queries/snippet.query.graphql'; const blobsDefault = []; -// eslint-disable-next-line import/prefer-default-export export const getSnippetMixin = { apollo: { snippet: { diff --git a/app/assets/javascripts/snippets/queries/snippet_visibility.query.graphql b/app/assets/javascripts/snippets/queries/snippet_visibility.query.graphql new file mode 100644 index 00000000000..5bd6c131bab --- /dev/null +++ b/app/assets/javascripts/snippets/queries/snippet_visibility.query.graphql @@ -0,0 +1,5 @@ +query defaultSnippetVisibility { + visibilityLevels @client + selectedLevel @client + multipleLevelsRestricted @client +} diff --git a/app/assets/javascripts/snippets/utils/blob.js b/app/assets/javascripts/snippets/utils/blob.js index fd5ff9a3d2e..21f52671801 100644 --- a/app/assets/javascripts/snippets/utils/blob.js +++ b/app/assets/javascripts/snippets/utils/blob.js @@ -4,6 +4,8 @@ import { SNIPPET_BLOB_ACTION_UPDATE, SNIPPET_BLOB_ACTION_MOVE, SNIPPET_BLOB_ACTION_DELETE, + SNIPPET_LEVELS_MAP, + SNIPPET_VISIBILITY, } from '../constants'; const createLocalId = () => uniqueId('blob_local_'); @@ -64,3 +66,16 @@ export const diffAll = (blobs, origBlobs) => { return [...deletedEntries, ...newEntries]; }; + +export const defaultSnippetVisibilityLevels = arr => { + if (Array.isArray(arr)) { + return arr.map(l => { + const translatedLevel = SNIPPET_LEVELS_MAP[l]; + return { + value: translatedLevel, + ...SNIPPET_VISIBILITY[translatedLevel], + }; + }); + } + return []; +}; diff --git a/app/assets/javascripts/static_site_editor/components/edit_area.vue b/app/assets/javascripts/static_site_editor/components/edit_area.vue index 53fbb2a330d..e602f26acdf 100644 --- a/app/assets/javascripts/static_site_editor/components/edit_area.vue +++ b/app/assets/javascripts/static_site_editor/components/edit_area.vue @@ -2,6 +2,7 @@ import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue'; import PublishToolbar from './publish_toolbar.vue'; import EditHeader from './edit_header.vue'; +import EditDrawer from './edit_drawer.vue'; import UnsavedChangesConfirmDialog from './unsaved_changes_confirm_dialog.vue'; import parseSourceFile from '~/static_site_editor/services/parse_source_file'; import { EDITOR_TYPES } from '~/vue_shared/components/rich_content_editor/constants'; @@ -15,6 +16,7 @@ export default { RichContentEditor, PublishToolbar, EditHeader, + EditDrawer, UnsavedChangesConfirmDialog, }, props: { @@ -48,6 +50,8 @@ export default { parsedSource: parseSourceFile(this.preProcess(true, this.content)), editorMode: EDITOR_TYPES.wysiwyg, isModified: false, + hasMatter: false, + isDrawerOpen: false, }; }, imageRepository: imageRepository(), @@ -55,10 +59,19 @@ export default { editableContent() { return this.parsedSource.content(this.isWysiwygMode); }, + editableMatter() { + return this.isDrawerOpen ? this.parsedSource.matter() : {}; + }, + hasSettings() { + return this.hasMatter && this.isWysiwygMode; + }, isWysiwygMode() { return this.editorMode === EDITOR_TYPES.wysiwyg; }, }, + created() { + this.refreshEditHelpers(); + }, methods: { preProcess(isWrap, value) { const formattedContent = formatter(value); @@ -67,9 +80,21 @@ export default { : templater.unwrap(formattedContent); return templatedContent; }, - onInputChange(newVal) { - this.parsedSource.sync(newVal, this.isWysiwygMode); + refreshEditHelpers() { this.isModified = this.parsedSource.isModified(); + this.hasMatter = this.parsedSource.hasMatter(); + }, + onDrawerOpen() { + this.isDrawerOpen = true; + this.refreshEditHelpers(); + }, + onDrawerClose() { + this.isDrawerOpen = false; + this.refreshEditHelpers(); + }, + onInputChange(newVal) { + this.parsedSource.syncContent(newVal, this.isWysiwygMode); + this.refreshEditHelpers(); }, onModeChange(mode) { this.editorMode = mode; @@ -77,6 +102,9 @@ export default { const preProcessedContent = this.preProcess(this.isWysiwygMode, this.editableContent); this.$refs.editor.resetInitialValue(preProcessedContent); }, + onUpdateSettings(settings) { + this.parsedSource.syncMatter(settings); + }, onUploadImage({ file, imageUrl }) { this.$options.imageRepository.add(file, imageUrl); }, @@ -93,12 +121,19 @@ export default { <template> <div class="d-flex flex-grow-1 flex-column h-100"> <edit-header class="py-2" :title="title" /> + <edit-drawer + v-if="hasMatter" + :is-open="isDrawerOpen" + :settings="editableMatter" + @close="onDrawerClose" + @updateSettings="onUpdateSettings" + /> <rich-content-editor ref="editor" :content="editableContent" :initial-edit-type="editorMode" :image-root="imageRoot" - class="mb-9 h-100" + class="mb-9 pb-6 h-100" @modeChange="onModeChange" @input="onInputChange" @uploadImage="onUploadImage" @@ -106,9 +141,11 @@ export default { <unsaved-changes-confirm-dialog :modified="isModified" /> <publish-toolbar class="gl-fixed gl-left-0 gl-bottom-0 gl-w-full" + :has-settings="hasSettings" :return-url="returnUrl" :saveable="isModified" :saving-changes="savingChanges" + @editSettings="onDrawerOpen" @submit="onSubmit" /> </div> diff --git a/app/assets/javascripts/static_site_editor/components/edit_drawer.vue b/app/assets/javascripts/static_site_editor/components/edit_drawer.vue new file mode 100644 index 00000000000..0484d38dde0 --- /dev/null +++ b/app/assets/javascripts/static_site_editor/components/edit_drawer.vue @@ -0,0 +1,32 @@ +<script> +import { GlDrawer } from '@gitlab/ui'; +import FrontMatterControls from './front_matter_controls.vue'; + +export default { + components: { + GlDrawer, + FrontMatterControls, + }, + props: { + isOpen: { + type: Boolean, + required: true, + }, + settings: { + type: Object, + required: true, + }, + }, +}; +</script> +<template> + <gl-drawer class="gl-pt-8" :open="isOpen" @close="$emit('close')"> + <template #header>{{ __('Page settings') }}</template> + <template> + <front-matter-controls + :settings="settings" + @updateSettings="$emit('updateSettings', $event)" + /> + </template> + </gl-drawer> +</template> diff --git a/app/assets/javascripts/static_site_editor/components/front_matter_controls.vue b/app/assets/javascripts/static_site_editor/components/front_matter_controls.vue new file mode 100644 index 00000000000..dad3907c3ff --- /dev/null +++ b/app/assets/javascripts/static_site_editor/components/front_matter_controls.vue @@ -0,0 +1,57 @@ +<script> +import { GlForm, GlFormInput, GlFormGroup } from '@gitlab/ui'; +import { humanize } from '~/lib/utils/text_utility'; + +export default { + components: { + GlForm, + GlFormInput, + GlFormGroup, + }, + props: { + settings: { + type: Object, + required: true, + }, + }, + data() { + return { + editableSettings: { ...this.settings }, + }; + }, + methods: { + getId(type, key) { + return `sse-front-matter-${type}-${key}`; + }, + getIsSupported(val) { + return ['string', 'number'].includes(typeof val); + }, + getLabel(str) { + return humanize(str); + }, + onUpdate() { + this.$emit('updateSettings', { ...this.editableSettings }); + }, + }, +}; +</script> +<template> + <gl-form> + <template v-for="(value, key) of editableSettings"> + <gl-form-group + v-if="getIsSupported(value)" + :id="getId('form-group', key)" + :key="key" + :label="getLabel(key)" + :label-for="getId('control', key)" + > + <gl-form-input + :id="getId('control', key)" + v-model.lazy="editableSettings[key]" + type="text" + @input="onUpdate" + /> + </gl-form-group> + </template> + </gl-form> +</template> diff --git a/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue b/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue index 6cd2a4dd700..2d62964cb3b 100644 --- a/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue +++ b/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue @@ -6,6 +6,11 @@ export default { GlButton, }, props: { + hasSettings: { + type: Boolean, + required: false, + default: false, + }, returnUrl: { type: String, required: false, @@ -31,12 +36,21 @@ export default { s__('StaticSiteEditor|Return to site') }}</gl-button> <gl-button + v-if="hasSettings" + ref="settings" + :disabled="savingChanges" + @click="$emit('editSettings')" + > + {{ __('Settings') }} + </gl-button> + <gl-button + ref="submit" variant="success" :disabled="!saveable" :loading="savingChanges" @click="$emit('submit')" > - <span>{{ __('Submit Changes') }}</span> + {{ __('Submit changes') }} </gl-button> </div> </div> diff --git a/app/assets/javascripts/static_site_editor/services/formatter.js b/app/assets/javascripts/static_site_editor/services/formatter.js index 92d5e8a5df8..9a5dcd307eb 100644 --- a/app/assets/javascripts/static_site_editor/services/formatter.js +++ b/app/assets/javascripts/static_site_editor/services/formatter.js @@ -1,3 +1,45 @@ +import { repeat } from 'lodash'; + +const topLevelOrderedRegexp = /^\d{1,3}/; +const nestedLineRegexp = /^\s+/; + +/** + * DISCLAIMER: This is a temporary fix that corrects the indentation + * spaces of list items. This workaround originates in the usage of + * the Static Site Editor to edit the Handbook. The Handbook uses a + * Markdown parser called Kramdown interprets lines indented + * with two spaces as content within a list. For example: + * + * 1. ordered list + * - nested unordered list + * + * The Static Site Editor uses a different Markdown parser based on the + * CommonMark specification (official Markdown spec) called ToastMark. + * When the SSE encounters a nested list with only two spaces, it flattens + * the list: + * + * 1. ordered list + * - nested unordered list + * + * This function attempts to correct this problem before the content is loaded + * by Toast UI. + */ +const correctNestedContentIndenation = source => { + const lines = source.split('\n'); + let topLevelOrderedListDetected = false; + + return lines + .reduce((result, line) => { + if (topLevelOrderedListDetected && nestedLineRegexp.test(line)) { + return [...result, line.replace(nestedLineRegexp, repeat(' ', 4))]; + } + + topLevelOrderedListDetected = topLevelOrderedRegexp.test(line); + return [...result, line]; + }, []) + .join('\n'); +}; + 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, @@ -8,7 +50,7 @@ const removeOrphanedBrTags = source => { }; const format = source => { - return removeOrphanedBrTags(source); + return correctNestedContentIndenation(removeOrphanedBrTags(source)); }; export default format; 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 edc69d0579a..25ab1084572 100644 --- a/app/assets/javascripts/static_site_editor/services/image_service.js +++ b/app/assets/javascripts/static_site_editor/services/image_service.js @@ -1,4 +1,3 @@ -// eslint-disable-next-line import/prefer-default-export export const getBinary = file => { return new Promise((resolve, reject) => { const reader = new FileReader(); 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 126dfe81b90..640186ee1d0 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,64 +1,40 @@ -const parseSourceFile = raw => { - const frontMatterRegex = /(^---$[\s\S]*?^---$)/m; - const preGroupedRegex = /([\s\S]*?)(^---$[\s\S]*?^---$)(\s*)([\s\S]*)/m; // preFrontMatter, frontMatter, spacing, and content - let initial; - let editable; - - const hasFrontMatter = source => frontMatterRegex.test(source); +import grayMatter from 'gray-matter'; - const buildPayload = (source, header, spacing, body) => { - return { raw: source, header, spacing, body }; - }; +const parseSourceFile = raw => { + const remake = source => grayMatter(source, {}); - const parse = source => { - if (hasFrontMatter(source)) { - const match = source.match(preGroupedRegex); - const [, preFrontMatter, frontMatter, spacing, content] = match; - const header = preFrontMatter + frontMatter; + let editable = remake(raw); - return buildPayload(source, header, spacing, content); + const syncContent = (newVal, isBody) => { + if (isBody) { + editable.content = newVal; + } else { + editable = remake(newVal); } - - return buildPayload(source, '', '', source); }; - const syncEditable = () => { - /* - We re-parse as markdown editing could have added non-body changes (preFrontMatter, frontMatter, or spacing). - Re-parsing additionally gets us the desired body that was extracted from the potentially mutated editable.raw - */ - editable = parse(editable.raw); - }; + const trimmedEditable = () => grayMatter.stringify(editable).trim(); - const syncBodyToRaw = () => { - editable.raw = `${editable.header}${editable.spacing}${editable.body}`; - }; - - const sync = (newVal, isBodyToRaw) => { - const editableKey = isBodyToRaw ? 'body' : 'raw'; - editable[editableKey] = newVal; + const content = (isBody = false) => (isBody ? editable.content.trim() : trimmedEditable()); // gray-matter internally adds an eof newline so we trim to bypass, open issue: https://github.com/jonschlinkert/gray-matter/issues/96 - if (isBodyToRaw) { - syncBodyToRaw(); - } - - syncEditable(); - }; + const matter = () => editable.data; - const content = (isBody = false) => { - const editableKey = isBody ? 'body' : 'raw'; - return editable[editableKey]; + const syncMatter = settings => { + const source = grayMatter.stringify(editable.content, settings); + syncContent(source); }; - const isModified = () => initial.raw !== editable.raw; + const isModified = () => trimmedEditable() !== raw; - initial = parse(raw); - editable = parse(raw); + const hasMatter = () => editable.matter.length > 0; return { + matter, + syncMatter, content, + syncContent, isModified, - sync, + hasMatter, }; }; diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js index 7206bbd7109..354ee00a977 100644 --- a/app/assets/javascripts/subscription_select.js +++ b/app/assets/javascripts/subscription_select.js @@ -1,11 +1,12 @@ import $ from 'jquery'; import { __ } from './locale'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default function subscriptionSelect() { $('.js-subscription-event').each((i, element) => { const fieldName = $(element).data('fieldName'); - return $(element).glDropdown({ + return initDeprecatedJQueryDropdown($(element), { selectable: true, fieldName, toggleLabel(selected, el, instance) { diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js index 10ad4170930..22bbd083a5d 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js +++ b/app/assets/javascripts/templates/issuable_template_selector.js @@ -33,7 +33,7 @@ export default class IssuableTemplateSelector extends TemplateSelector { this.templateWarningEl.find('.js-close-btn').on('click', () => { // Explicitly check against 0 value if (this.previousSelectedIndex !== undefined) { - this.dropdown.data('glDropdown').selectRowAtIndex(this.previousSelectedIndex); + this.dropdown.data('deprecatedJQueryDropdown').selectRowAtIndex(this.previousSelectedIndex); } else { this.reset(); } @@ -61,7 +61,7 @@ export default class IssuableTemplateSelector extends TemplateSelector { } setSelectedIndex() { - this.previousSelectedIndex = this.dropdown.data('glDropdown').selectedIndex; + this.previousSelectedIndex = this.dropdown.data('deprecatedJQueryDropdown').selectedIndex; } onDropdownClicked(query) { diff --git a/app/assets/javascripts/tooltips/components/tooltips.vue b/app/assets/javascripts/tooltips/components/tooltips.vue new file mode 100644 index 00000000000..8307f878def --- /dev/null +++ b/app/assets/javascripts/tooltips/components/tooltips.vue @@ -0,0 +1,116 @@ +<script> +import { GlTooltip, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; +import { uniqueId } from 'lodash'; + +const getTooltipTitle = element => { + return element.getAttribute('title') || element.dataset.title; +}; + +const newTooltip = (element, config = {}) => { + const { placement, container, boundary, html, triggers } = element.dataset; + const title = getTooltipTitle(element); + + return { + id: uniqueId('gl-tooltip'), + target: element, + title, + html, + placement, + container, + boundary, + triggers, + disabled: !title, + ...config, + }; +}; + +export default { + components: { + GlTooltip, + }, + directives: { + SafeHtml, + }, + data() { + return { + tooltips: [], + }; + }, + created() { + this.observer = new MutationObserver(mutations => { + mutations.forEach(mutation => { + mutation.removedNodes.forEach(this.dispose); + }); + }); + }, + beforeDestroy() { + this.observer.disconnect(); + }, + methods: { + addTooltips(elements, config) { + const newTooltips = elements + .filter(element => !this.tooltipExists(element)) + .map(element => newTooltip(element, config)); + + newTooltips.forEach(tooltip => this.observe(tooltip)); + + this.tooltips.push(...newTooltips); + }, + observe(tooltip) { + this.observer.observe(tooltip.target.parentElement, { + childList: true, + }); + }, + dispose(target) { + if (!target) { + this.tooltips = []; + } else { + const index = this.tooltips.indexOf(this.findTooltipByTarget(target)); + + if (index > -1) { + this.tooltips.splice(index, 1); + } + } + }, + fixTitle(target) { + const tooltip = this.findTooltipByTarget(target); + + if (tooltip) { + tooltip.title = target.getAttribute('title'); + } + }, + triggerEvent(target, event) { + const tooltip = this.findTooltipByTarget(target); + + if (tooltip) { + this.$refs[tooltip.id][0].$emit(event); + } + }, + tooltipExists(element) { + return Boolean(this.findTooltipByTarget(element)); + }, + findTooltipByTarget(element) { + return this.tooltips.find(tooltip => tooltip.target === element); + }, + }, +}; +</script> +<template> + <div> + <gl-tooltip + v-for="(tooltip, index) in tooltips" + :id="tooltip.id" + :ref="tooltip.id" + :key="index" + :target="tooltip.target" + :triggers="tooltip.triggers" + :placement="tooltip.placement" + :container="tooltip.container" + :boundary="tooltip.boundary" + :disabled="tooltip.disabled" + > + <span v-if="tooltip.html" v-safe-html="tooltip.title"></span> + <span v-else>{{ tooltip.title }}</span> + </gl-tooltip> + </div> +</template> diff --git a/app/assets/javascripts/tooltips/index.js b/app/assets/javascripts/tooltips/index.js new file mode 100644 index 00000000000..cfbd88d6c40 --- /dev/null +++ b/app/assets/javascripts/tooltips/index.js @@ -0,0 +1,120 @@ +import Vue from 'vue'; +import jQuery from 'jquery'; +import { toArray, isFunction } from 'lodash'; +import Tooltips from './components/tooltips.vue'; + +let app; + +const EVENTS_MAP = { + hover: 'mouseenter', + click: 'click', + focus: 'focus', +}; + +const DEFAULT_TRIGGER = 'hover focus'; +const APP_ELEMENT_ID = 'gl-tooltips-app'; + +const tooltipsApp = () => { + if (!app) { + const container = document.createElement('div'); + + container.setAttribute('id', APP_ELEMENT_ID); + document.body.appendChild(container); + + app = new Vue({ + render(h) { + return h(Tooltips, { + props: { + elements: this.elements, + }, + ref: 'tooltips', + }); + }, + }).$mount(container); + } + + return app.$refs.tooltips; +}; + +const isTooltip = (node, selector) => node.matches && node.matches(selector); + +const addTooltips = (elements, config) => { + tooltipsApp().addTooltips(toArray(elements), config); +}; + +const handleTooltipEvent = (rootTarget, e, selector, config = {}) => { + for (let { target } = e; target && target !== rootTarget; target = target.parentNode) { + if (isTooltip(target, selector)) { + addTooltips([target], { + show: true, + ...config, + }); + break; + } + } +}; + +const applyToElements = (elements, handler) => toArray(elements).forEach(handler); + +const invokeBootstrapApi = (elements, method) => { + if (isFunction(elements.tooltip)) { + jQuery(elements).tooltip(method); + } +}; + +const isGlTooltipsEnabled = () => Boolean(window.gon.glTooltipsEnabled); + +const tooltipApiInvoker = ({ glHandler, bsHandler }) => (elements, ...params) => { + if (isGlTooltipsEnabled()) { + applyToElements(elements, glHandler); + } else { + bsHandler(elements, ...params); + } +}; + +export const initTooltips = (config = {}) => { + if (isGlTooltipsEnabled()) { + const triggers = config?.triggers || DEFAULT_TRIGGER; + const events = triggers.split(' ').map(trigger => EVENTS_MAP[trigger]); + + events.forEach(event => { + document.addEventListener( + event, + e => handleTooltipEvent(document, e, config.selector, config), + true, + ); + }); + + return tooltipsApp(); + } + + return invokeBootstrapApi(document.body, config); +}; +export const dispose = tooltipApiInvoker({ + glHandler: element => tooltipsApp().dispose(element), + bsHandler: elements => invokeBootstrapApi(elements, 'dispose'), +}); +export const fixTitle = tooltipApiInvoker({ + 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'), +}); +export const disable = tooltipApiInvoker({ + 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'), +}); +export const show = tooltipApiInvoker({ + glHandler: element => tooltipsApp().triggerEvent(element, 'open'), + bsHandler: elements => invokeBootstrapApi(elements, 'show'), +}); +export const destroy = () => { + tooltipsApp().$destroy(); + app = null; +}; diff --git a/app/assets/javascripts/tracking.js b/app/assets/javascripts/tracking.js index 10510595570..37ebe6b6c4d 100644 --- a/app/assets/javascripts/tracking.js +++ b/app/assets/javascripts/tracking.js @@ -126,14 +126,18 @@ export function initUserTracking() { const opts = { ...DEFAULT_SNOWPLOW_OPTIONS, ...window.snowplowOptions }; window.snowplow('newTracker', opts.namespace, opts.hostname, opts); + document.dispatchEvent(new Event('SnowplowInitialized')); +} + +export function initDefaultTrackers() { + if (!Tracking.enabled()) return; + window.snowplow('enableActivityTracking', 30, 30); window.snowplow('trackPageView'); // must be after enableActivityTracking - if (opts.formTracking) window.snowplow('enableFormTracking'); - if (opts.linkClickTracking) window.snowplow('enableLinkClickTracking'); + if (window.snowplowOptions.formTracking) window.snowplow('enableFormTracking'); + if (window.snowplowOptions.linkClickTracking) window.snowplow('enableLinkClickTracking'); Tracking.bindDocument(); Tracking.trackLoadEvents(); - - document.dispatchEvent(new Event('SnowplowInitialized')); } diff --git a/app/assets/javascripts/ui_development_kit.js b/app/assets/javascripts/ui_development_kit.js index be18ac5da24..4f32e143de8 100644 --- a/app/assets/javascripts/ui_development_kit.js +++ b/app/assets/javascripts/ui_development_kit.js @@ -1,8 +1,9 @@ import $ from 'jquery'; import Api from './api'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; export default () => { - $('#js-project-dropdown').glDropdown({ + initDeprecatedJQueryDropdown($('#js-project-dropdown'), { data: (term, callback) => { Api.projects( term, diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js index e45b0de9083..5f4260f26ff 100644 --- a/app/assets/javascripts/users_select/index.js +++ b/app/assets/javascripts/users_select/index.js @@ -11,8 +11,9 @@ import { import axios from '../lib/utils/axios_utils'; import { s__, __, sprintf } from '../locale'; import ModalStore from '../boards/stores/modal_store'; -import { parseBoolean } from '../lib/utils/common_utils'; +import { parseBoolean, spriteIcon } from '../lib/utils/common_utils'; import { getAjaxUsersSelectOptions, getAjaxUsersSelectParams } from './utils'; +import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; // TODO: remove eventHub hack after code splitting refactor window.emitSidebarEvent = window.emitSidebarEvent || $.noop; @@ -54,6 +55,7 @@ function UsersSelect(currentUser, els, options = {}) { const defaultLabel = $dropdown.data('defaultLabel'); const issueURL = $dropdown.data('issueUpdate'); const $selectbox = $dropdown.closest('.selectbox'); + const $assignToMeLink = $selectbox.next('.assign-to-me-link'); let $block = $selectbox.closest('.block'); const abilityName = $dropdown.data('abilityName'); let $value = $block.find('.value'); @@ -160,7 +162,7 @@ function UsersSelect(currentUser, els, options = {}) { }); }; - $('.assign-to-me-link').on('click', e => { + $assignToMeLink.on('click', e => { e.preventDefault(); $(e.currentTarget).hide(); @@ -224,7 +226,9 @@ function UsersSelect(currentUser, els, options = {}) { }); }; collapsedAssigneeTemplate = template( - '<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>', + `<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> ${spriteIcon( + 'user', + )} <% } %>`, ); assigneeTemplate = template( `<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> @@ -233,14 +237,14 @@ function UsersSelect(currentUser, els, options = {}) { closingTag: '</a>', })}</span> <% } %>`, ); - return $dropdown.glDropdown({ + return initDeprecatedJQueryDropdown($dropdown, { showMenuAbove, data(term, callback) { return userSelect.users(term, options, users => { // GitLabDropdownFilter returns this.instance // GitLabDropdownRemote returns this.options.instance - const glDropdown = this.instance || this.options.instance; - glDropdown.options.processData(term, users, callback); + const deprecatedJQueryDropdown = this.instance || this.options.instance; + deprecatedJQueryDropdown.options.processData(term, users, callback); }); }, processData(term, data, callback) { @@ -349,7 +353,7 @@ function UsersSelect(currentUser, els, options = {}) { callback(users); if (showMenuAbove) { - $dropdown.data('glDropdown').positionMenuAbove(); + $dropdown.data('deprecatedJQueryDropdown').positionMenuAbove(); } }, filterable: true, @@ -359,13 +363,13 @@ function UsersSelect(currentUser, els, options = {}) { }, selectable: true, fieldName: $dropdown.data('fieldName'), - toggleLabel(selected, el, glDropdown) { - const inputValue = glDropdown.filterInput.val(); + toggleLabel(selected, el, deprecatedJQueryDropdown) { + const inputValue = deprecatedJQueryDropdown.filterInput.val(); if (this.multiSelect && inputValue === '') { // Remove non-users from the fullData array - const users = glDropdown.filteredFullData(); - const callback = glDropdown.parseData.bind(glDropdown); + const users = deprecatedJQueryDropdown.filteredFullData(); + const callback = deprecatedJQueryDropdown.parseData.bind(deprecatedJQueryDropdown); // Update the data model this.processData(inputValue, users, callback); @@ -448,9 +452,9 @@ function UsersSelect(currentUser, els, options = {}) { } if (getSelected().find(u => u === gon.current_user_id)) { - $('.assign-to-me-link').hide(); + $assignToMeLink.hide(); } else { - $('.assign-to-me-link').show(); + $assignToMeLink.show(); } } @@ -557,92 +561,99 @@ function UsersSelect(currentUser, els, options = {}) { }, }); }); - import(/* webpackChunkName: 'select2' */ 'select2/select2') - .then(() => { - $('.ajax-users-select').each((i, select) => { - const options = getAjaxUsersSelectOptions($(select), AJAX_USERS_SELECT_OPTIONS_MAP); - options.skipLdap = $(select).hasClass('skip_ldap'); - const showNullUser = $(select).data('nullUser'); - const showAnyUser = $(select).data('anyUser'); - const showEmailUser = $(select).data('emailUser'); - const firstUser = $(select).data('firstUser'); - return $(select).select2({ - placeholder: __('Search for a user'), - multiple: $(select).hasClass('multiselect'), - minimumInputLength: 0, - query(query) { - return userSelect.users(query.term, options, users => { - let name; - const data = { - results: users, - }; - if (query.term.length === 0) { - if (firstUser) { - // Move current user to the front of the list - const ref = data.results; - - for (let index = 0, len = ref.length; index < len; index += 1) { - const obj = ref[index]; - if (obj.username === firstUser) { - data.results.splice(index, 1); - data.results.unshift(obj); - break; + + if ($('.ajax-users-select').length) { + import(/* webpackChunkName: 'select2' */ 'select2/select2') + .then(() => { + $('.ajax-users-select').each((i, select) => { + const options = getAjaxUsersSelectOptions($(select), AJAX_USERS_SELECT_OPTIONS_MAP); + options.skipLdap = $(select).hasClass('skip_ldap'); + const showNullUser = $(select).data('nullUser'); + const showAnyUser = $(select).data('anyUser'); + const showEmailUser = $(select).data('emailUser'); + const firstUser = $(select).data('firstUser'); + return $(select).select2({ + placeholder: __('Search for a user'), + multiple: $(select).hasClass('multiselect'), + minimumInputLength: 0, + query(query) { + return userSelect.users(query.term, options, users => { + let name; + const data = { + results: users, + }; + if (query.term.length === 0) { + if (firstUser) { + // Move current user to the front of the list + const ref = data.results; + + for (let index = 0, len = ref.length; index < len; index += 1) { + const obj = ref[index]; + if (obj.username === firstUser) { + data.results.splice(index, 1); + data.results.unshift(obj); + break; + } } } - } - if (showNullUser) { - const nullUser = { - name: s__('UsersSelect|Unassigned'), - id: 0, - }; - data.results.unshift(nullUser); - } - if (showAnyUser) { - name = showAnyUser; - if (name === true) { - name = s__('UsersSelect|Any User'); + if (showNullUser) { + const nullUser = { + name: s__('UsersSelect|Unassigned'), + id: 0, + }; + data.results.unshift(nullUser); + } + if (showAnyUser) { + name = showAnyUser; + if (name === true) { + name = s__('UsersSelect|Any User'); + } + const anyUser = { + name, + id: null, + }; + data.results.unshift(anyUser); } - const anyUser = { - name, - id: null, + } + if ( + showEmailUser && + data.results.length === 0 && + query.term.match(/^[^@]+@[^@]+$/) + ) { + const trimmed = query.term.trim(); + const emailUser = { + name: sprintf(__('Invite "%{trimmed}" by email'), { trimmed }), + username: trimmed, + id: trimmed, + invite: true, }; - data.results.unshift(anyUser); + data.results.unshift(emailUser); } - } - if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) { - const trimmed = query.term.trim(); - const emailUser = { - name: sprintf(__('Invite "%{trimmed}" by email'), { trimmed }), - username: trimmed, - id: trimmed, - invite: true, - }; - data.results.unshift(emailUser); - } - return query.callback(data); - }); - }, - initSelection() { - const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; - return userSelect.initSelection.apply(userSelect, args); - }, - formatResult() { - const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; - return userSelect.formatResult.apply(userSelect, args); - }, - formatSelection() { - const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; - return userSelect.formatSelection.apply(userSelect, args); - }, - dropdownCssClass: 'ajax-users-dropdown', - // we do not want to escape markup since we are displaying html in results - escapeMarkup(m) { - return m; - }, + return query.callback(data); + }); + }, + initSelection() { + const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; + return userSelect.initSelection.apply(userSelect, args); + }, + formatResult() { + const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; + return userSelect.formatResult.apply(userSelect, args); + }, + formatSelection() { + const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; + return userSelect.formatSelection.apply(userSelect, args); + }, + dropdownCssClass: 'ajax-users-dropdown', + // we do not want to escape markup since we are displaying html in results + escapeMarkup(m) { + return m; + }, + }); }); - }); - }) - .catch(() => {}); + }) + .catch(() => {}); + } } UsersSelect.prototype.initSelection = function(element, callback) { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue index 24cd9d6428d..55fa24fb51a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue @@ -1,11 +1,10 @@ <script> -import { GlTooltipDirective, GlLink } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui'; export default { components: { GlLink, - Icon, + GlIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -35,7 +34,7 @@ export default { target="_blank" class="d-flex-center pl-1" > - <icon name="question" /> + <gl-icon name="question" /> </gl-link> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js b/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js index a7ab11290eb..66de4f8b682 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js @@ -11,3 +11,9 @@ export const CANCELED = 'canceled'; export const STOPPING = 'stopping'; export const DEPLOYING = 'deploying'; export const REDEPLOYING = 'redeploying'; + +export const ACT_BUTTON_ICONS = { + play: 'play', + repeat: 'repeat', + stop: 'stop', +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_action_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_action_button.vue index 7d74d5531b4..cc3efae565a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_action_button.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_action_button.vue @@ -1,12 +1,12 @@ <script> -import { GlTooltipDirective, GlDeprecatedButton } from '@gitlab/ui'; +import { GlTooltipDirective, GlButton } from '@gitlab/ui'; import { __ } from '~/locale'; import { RUNNING } from './constants'; export default { name: 'DeploymentActionButton', components: { - GlDeprecatedButton, + GlButton, }, directives: { GlTooltip: GlTooltipDirective, @@ -35,6 +35,10 @@ export default { required: false, default: '', }, + icon: { + type: String, + required: true, + }, }, computed: { isActionInProgress() { @@ -58,18 +62,19 @@ export default { </script> <template> - <span v-gl-tooltip :title="actionInProgressTooltip" class="d-inline-block" tabindex="0"> - <gl-deprecated-button + <span v-gl-tooltip :title="actionInProgressTooltip" class="gl-display-inline-block" tabindex="0"> + <gl-button v-gl-tooltip + category="primary" + size="small" :title="buttonTitle" :loading="isLoading" :disabled="isActionInProgress" - :class="`btn btn-default btn-sm inline gl-ml-2 ${containerClasses}`" + :class="`inline gl-ml-2 ${containerClasses}`" + :icon="icon" @click="$emit('click')" > - <span class="d-inline-flex align-items-baseline"> - <slot> </slot> - </span> - </gl-deprecated-button> + <slot> </slot> + </gl-button> </span> </template> 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 af0b4087d46..208df03b6a4 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 @@ -1,5 +1,4 @@ <script> -import { GlIcon } from '@gitlab/ui'; import { __, s__ } from '~/locale'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { visitUrl } from '~/lib/utils/url_utility'; @@ -7,14 +6,22 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import MRWidgetService from '../../services/mr_widget_service'; import DeploymentActionButton from './deployment_action_button.vue'; import DeploymentViewButton from './deployment_view_button.vue'; -import { MANUAL_DEPLOY, FAILED, SUCCESS, STOPPING, DEPLOYING, REDEPLOYING } from './constants'; +import { + MANUAL_DEPLOY, + FAILED, + SUCCESS, + STOPPING, + DEPLOYING, + REDEPLOYING, + ACT_BUTTON_ICONS, +} from './constants'; export default { name: 'DeploymentActions', + btnIcons: ACT_BUTTON_ICONS, components: { DeploymentActionButton, DeploymentViewButton, - GlIcon, }, mixins: [glFeatureFlagsMixin()], props: { @@ -151,10 +158,10 @@ export default { :action-in-progress="actionInProgress" :actions-configuration="$options.actionsConfiguration[constants.DEPLOYING]" :computed-deployment-status="computedDeploymentStatus" + :icon="$options.btnIcons.play" container-classes="js-manual-deploy-action" @click="deployManually" > - <gl-icon name="play" /> <span>{{ $options.actionsConfiguration[constants.DEPLOYING].buttonText }}</span> </deployment-action-button> <deployment-action-button @@ -162,10 +169,10 @@ export default { :action-in-progress="actionInProgress" :actions-configuration="$options.actionsConfiguration[constants.REDEPLOYING]" :computed-deployment-status="computedDeploymentStatus" + :icon="$options.btnIcons.repeat" container-classes="js-manual-redeploy-action" @click="redeploy" > - <gl-icon name="repeat" /> <span>{{ $options.actionsConfiguration[constants.REDEPLOYING].buttonText }}</span> </deployment-action-button> <deployment-view-button @@ -181,10 +188,9 @@ export default { :computed-deployment-status="computedDeploymentStatus" :actions-configuration="$options.actionsConfiguration[constants.STOPPING]" :button-title="$options.actionsConfiguration[constants.STOPPING].buttonText" + :icon="$options.btnIcons.stop" container-classes="js-stop-env" @click="stopEnvironment" - > - <gl-icon name="stop" /> - </deployment-action-button> + /> </div> </template> 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 b12250d1d1c..157d6d60290 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 @@ -1,17 +1,23 @@ <script> -import { GlLink } from '@gitlab/ui'; -import FilteredSearchDropdown from '~/vue_shared/components/filtered_search_dropdown.vue'; +import { GlButtonGroup, GlDropdown, GlDropdownItem, GlLink, GlSearchBoxByType } from '@gitlab/ui'; +import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; import ReviewAppLink from '../review_app_link.vue'; export default { name: 'DeploymentViewButton', components: { - FilteredSearchDropdown, + GlButtonGroup, + GlDropdown, + GlDropdownItem, GlLink, + GlSearchBoxByType, ReviewAppLink, VisualReviewAppLink: () => import('ee_component/vue_merge_request_widget/components/visual_review_app_link.vue'), }, + directives: { + autofocusonshow, + }, props: { appButtonText: { type: Object, @@ -37,6 +43,9 @@ export default { }), }, }, + data() { + return { searchTerm: '' }; + }, computed: { deploymentExternalUrl() { if (this.deployment.changes && this.deployment.changes.length === 1) { @@ -47,44 +56,52 @@ export default { shouldRenderDropdown() { return this.deployment.changes && this.deployment.changes.length > 1; }, + filteredChanges() { + return this.deployment?.changes?.filter(change => change.path.includes(this.searchTerm)); + }, }, }; </script> - <template> <span> - <filtered-search-dropdown - v-if="shouldRenderDropdown" - class="js-mr-wigdet-deployment-dropdown inline" - :items="deployment.changes" - :main-action-link="deploymentExternalUrl" - filter-key="path" - > - <template #mainAction="{ className }"> - <review-app-link - :display="appButtonText" - :link="deploymentExternalUrl" - :css-class="`deploy-link js-deploy-url inline ${className}`" + <gl-button-group v-if="shouldRenderDropdown" size="small"> + <review-app-link + :display="appButtonText" + :link="deploymentExternalUrl" + size="small" + css-class="deploy-link js-deploy-url inline" + /> + <gl-dropdown size="small" class="js-mr-wigdet-deployment-dropdown"> + <gl-search-box-by-type + v-model.trim="searchTerm" + v-autofocusonshow + autofocus + class="gl-m-3" /> - </template> - - <template #result="{ result }"> - <gl-link - :href="result.external_url" - target="_blank" - rel="noopener noreferrer nofollow" - class="js-deploy-url-menu-item menu-item" + <gl-dropdown-item + v-for="change in filteredChanges" + :key="change.path" + class="js-filtered-dropdown-result" > - <strong class="str-truncated-100 gl-mb-0 d-block">{{ result.path }}</strong> - - <p class="text-secondary str-truncated-100 gl-mb-0 d-block">{{ result.external_url }}</p> - </gl-link> - </template> - </filtered-search-dropdown> + <gl-link + :href="change.external_url" + target="_blank" + rel="noopener noreferrer nofollow" + class="js-deploy-url-menu-item menu-item" + > + <strong class="str-truncated-100 gl-mb-0 gl-display-block">{{ change.path }}</strong> + <p class="text-secondary str-truncated-100 gl-mb-0 d-block"> + {{ change.external_url }} + </p> + </gl-link> + </gl-dropdown-item> + </gl-dropdown> + </gl-button-group> <review-app-link v-else :display="appButtonText" :link="deploymentExternalUrl" + size="small" css-class="js-deploy-url deploy-link btn btn-default btn-sm inline" /> <visual-review-app-link 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 fe41a15979e..9b2cd41092e 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 @@ -1,5 +1,6 @@ <script> -import { sprintf, s__ } from '~/locale'; +import { GlLoadingIcon, GlSprintf, GlLink } from '@gitlab/ui'; +import { s__ } from '~/locale'; import statusCodes from '~/lib/utils/http_status'; import { bytesToMiB } from '~/lib/utils/number_utils'; import { backOff } from '~/lib/utils/common_utils'; @@ -10,6 +11,9 @@ export default { name: 'MemoryUsage', components: { MemoryGraph, + GlLoadingIcon, + GlSprintf, + GlLink, }, props: { metricsUrl: { @@ -47,45 +51,22 @@ export default { return !this.loadingMetrics && !this.hasMetrics && !this.loadFailed; }, memoryChangeMessage() { - const messageProps = { - memoryFrom: this.memoryFrom, - memoryTo: this.memoryTo, - metricsLinkStart: `<a href="${this.metricsMonitoringUrl}">`, - metricsLinkEnd: '</a>', - emphasisStart: '<b>', - emphasisEnd: '</b>', - }; const memoryTo = Number(this.memoryTo); const memoryFrom = Number(this.memoryFrom); - let memoryUsageMsg = ''; if (memoryTo > memoryFrom) { - memoryUsageMsg = sprintf( - s__( - 'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB', - ), - messageProps, - false, + return s__( + 'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB', ); } else if (memoryTo < memoryFrom) { - memoryUsageMsg = sprintf( - s__( - 'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB', - ), - messageProps, - false, - ); - } else { - memoryUsageMsg = sprintf( - s__( - 'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB', - ), - messageProps, - false, + return s__( + 'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB', ); } - return memoryUsageMsg; + return s__( + 'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB', + ); }, }, mounted() { @@ -155,14 +136,23 @@ export default { <template> <div class="mr-info-list clearfix mr-memory-usage js-mr-memory-usage"> <p v-if="shouldShowLoading" class="usage-info js-usage-info usage-info-loading"> - <i class="fa fa-spinner fa-spin usage-info-load-spinner" aria-hidden="true"> </i - >{{ s__('mrWidget|Loading deployment statistics') }} + <gl-loading-icon class="usage-info-load-spinner" />{{ + s__('mrWidget|Loading deployment statistics') + }} + </p> + <p v-if="shouldShowMemoryGraph" class="usage-info js-usage-info"> + <gl-sprintf :message="memoryChangeMessage"> + <template #metricsLink="{ content }"> + <gl-link :href="metricsMonitoringUrl">{{ content }}</gl-link> + </template> + <template #emphasis="{content}"> + <strong>{{ content }}</strong> + </template> + <template #memoryFrom>{{ memoryFrom }}</template> + <template #memoryTo>{{ memoryTo }}</template> + </gl-sprintf> </p> - <p - v-if="shouldShowMemoryGraph" - class="usage-info js-usage-info" - v-html="memoryChangeMessage" - ></p> + <p v-if="shouldShowLoadFailure" class="usage-info js-usage-info usage-info-failed"> {{ s__('mrWidget|Failed to load deployment statistics') }} </p> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue index 24174c29d51..b6b5b56e5aa 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue @@ -1,13 +1,12 @@ <script> -import { GlButton, GlLoadingIcon } from '@gitlab/ui'; +import { GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; export default { components: { GlButton, GlLoadingIcon, - Icon, + GlIcon, }, props: { title: { @@ -66,7 +65,7 @@ export default { @click="toggleCollapsed" > <gl-loading-icon v-if="isLoading" /> - <icon v-else :name="arrowIconName" class="js-icon" /> + <gl-icon v-else :name="arrowIconName" class="js-icon" /> </button> <gl-button variant="link" 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 19a222462b3..a2636ce52ad 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 @@ -1,13 +1,12 @@ <script> -import { GlLink } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlLink, GlIcon } from '@gitlab/ui'; import { WARNING, DANGER, WARNING_MESSAGE_CLASS, DANGER_MESSAGE_CLASS } from '../constants'; export default { name: 'MrWidgetAlertMessage', components: { GlLink, - Icon, + GlIcon, }, props: { type: { @@ -40,7 +39,7 @@ export default { <div class="m-3 ml-7" :class="messageClass"> <slot></slot> <gl-link v-if="helpPath" :href="helpPath" target="_blank"> - <icon :size="16" name="question-o" class="align-middle" /> + <gl-icon :size="16" name="question-o" class="align-middle" /> </gl-link> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue index 897f706290d..814d4e8341e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue @@ -1,24 +1,33 @@ <script> +/* eslint-disable vue/no-v-html */ import Mousetrap from 'mousetrap'; import { escape } from 'lodash'; +import { + GlButton, + GlDropdown, + GlDropdownSectionHeader, + GlDropdownItem, + GlTooltipDirective, +} from '@gitlab/ui'; import { n__, s__, sprintf } from '~/locale'; import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility'; -import Icon from '~/vue_shared/components/icon.vue'; import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import tooltip from '~/vue_shared/directives/tooltip'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import MrWidgetIcon from './mr_widget_icon.vue'; export default { name: 'MRWidgetHeader', components: { - Icon, clipboardButton, TooltipOnTruncate, MrWidgetIcon, + GlButton, + GlDropdown, + GlDropdownSectionHeader, + GlDropdownItem, }, directives: { - tooltip, + GlTooltip: GlTooltipDirective, }, props: { mr: { @@ -124,62 +133,59 @@ export default { <div class="branch-actions d-flex"> <template v-if="mr.isOpen"> - <a + <span v-if="!mr.sourceBranchRemoved" - v-tooltip - :href="webIdePath" + v-gl-tooltip :title="ideButtonTitle" - :class="{ disabled: !mr.canPushToSourceBranch }" - class="btn btn-default js-web-ide d-none d-md-inline-block gl-mr-3" - data-placement="bottom" - tabindex="0" - role="button" - data-qa-selector="open_in_web_ide_button" + class="gl-display-none d-md-inline-block gl-mr-3" + :tabindex="!mr.canPushToSourceBranch ? 0 : null" > - {{ s__('mrWidget|Open in Web IDE') }} - </a> - <button + <gl-button + :href="webIdePath" + :disabled="!mr.canPushToSourceBranch" + class="js-web-ide" + tabindex="0" + role="button" + data-qa-selector="open_in_web_ide_button" + > + {{ s__('mrWidget|Open in Web IDE') }} + </gl-button> + </span> + <gl-button :disabled="mr.sourceBranchRemoved" data-target="#modal_merge_info" data-toggle="modal" - class="btn btn-default js-check-out-branch gl-mr-3" - type="button" + class="js-check-out-branch gl-mr-3" > {{ s__('mrWidget|Check out branch') }} - </button> + </gl-button> </template> - <span class="dropdown"> - <button - type="button" - class="btn dropdown-toggle qa-dropdown-toggle" - data-toggle="dropdown" - :aria-label="__('Download as')" - aria-haspopup="true" - aria-expanded="false" + <gl-dropdown + v-gl-tooltip + :title="__('Download as')" + :aria-label="__('Download as')" + icon="download" + right + data-qa-selector="download_dropdown" + > + <gl-dropdown-section-header>{{ s__('Download as') }}</gl-dropdown-section-header> + <gl-dropdown-item + :href="mr.emailPatchesPath" + class="js-download-email-patches" + download + data-qa-selector="download_email_patches" > - <icon name="download" /> <i class="fa fa-caret-down" aria-hidden="true"> </i> - </button> - <ul class="dropdown-menu dropdown-menu-right"> - <li> - <a - :href="mr.emailPatchesPath" - class="js-download-email-patches qa-download-email-patches" - download - > - {{ s__('mrWidget|Email patches') }} - </a> - </li> - <li> - <a - :href="mr.plainDiffPath" - class="js-download-plain-diff qa-download-plain-diff" - download - > - {{ s__('mrWidget|Plain diff') }} - </a> - </li> - </ul> - </span> + {{ s__('mrWidget|Email patches') }} + </gl-dropdown-item> + <gl-dropdown-item + :href="mr.plainDiffPath" + class="js-download-plain-diff" + download + data-qa-selector="download_plain_diff" + > + {{ s__('mrWidget|Plain diff') }} + </gl-dropdown-item> + </gl-dropdown> </div> </div> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue index e1659d9a167..472df8e3110 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue @@ -1,8 +1,8 @@ <script> -import Icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; export default { - components: { Icon }, + components: { GlIcon }, props: { name: { type: String, @@ -14,6 +14,6 @@ export default { <template> <div class="circle-icon-container gl-mr-3 align-self-start align-self-lg-center"> - <icon :name="name" :size="24" /> + <gl-icon :name="name" :size="24" /> </div> </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 7326bd0804d..5066a88b52b 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 @@ -1,8 +1,15 @@ <script> -/* eslint-disable vue/require-default-prop */ -import { GlIcon, GlLink, GlLoadingIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; +/* eslint-disable vue/require-default-prop, vue/no-v-html */ +import { + GlIcon, + GlLink, + GlLoadingIcon, + GlSprintf, + GlTooltip, + GlTooltipDirective, +} from '@gitlab/ui'; import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline'; -import { s__ } from '~/locale'; +import { s__, n__ } from '~/locale'; import PipelineStage from '~/pipelines/components/pipelines_list/stage.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; @@ -15,6 +22,7 @@ export default { GlLoadingIcon, GlIcon, GlSprintf, + GlTooltip, PipelineStage, TooltipOnTruncate, LinkedPipelinesMiniList: () => @@ -33,6 +41,11 @@ export default { type: String, required: false, }, + buildsWithCoverage: { + type: Array, + required: false, + default: () => [], + }, // This prop needs to be camelCase, html attributes are case insensive // https://vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case hasCi: { @@ -100,6 +113,16 @@ export default { } return ''; }, + pipelineCoverageJobNumberText() { + return n__('from %d job', 'from %d jobs', this.buildsWithCoverage.length); + }, + pipelineCoverageTooltipDescription() { + return n__( + 'Coverage value for this pipeline was calculated by the coverage value of %d job.', + 'Coverage value for this pipeline was calculated by averaging the resulting coverage values of %d jobs.', + this.buildsWithCoverage.length, + ); + }, }, errorText: s__( 'Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation%{linkEnd}.', @@ -139,7 +162,7 @@ export default { > <gl-icon name="question" - :small="12" + :size="12" tabindex="0" role="text" :aria-label="__('Link to go to GitLab pipeline documentation')" @@ -189,14 +212,30 @@ export default { </div> <div v-if="pipeline.coverage" class="coverage" data-testid="pipeline-coverage"> {{ s__('Pipeline|Coverage') }} {{ pipeline.coverage }}% - <span v-if="pipelineCoverageDelta" :class="coverageDeltaClass" data-testid="pipeline-coverage-delta" + >({{ pipelineCoverageDelta }}%)</span > - ({{ pipelineCoverageDelta }}%) + + {{ pipelineCoverageJobNumberText }} + <span ref="pipelineCoverageQuestion"> + <gl-icon name="question" :size="12" /> </span> + <gl-tooltip + :target="() => $refs.pipelineCoverageQuestion" + data-testid="pipeline-coverage-tooltip" + > + {{ pipelineCoverageTooltipDescription }} + <div + v-for="(build, index) in buildsWithCoverage" + :key="`${build.name}-${index}`" + class="gl-mt-3 gl-text-left gl-px-4" + > + {{ build.name }} ({{ build.coverage }}%) + </div> + </gl-tooltip> </div> </div> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue index 5c307b5ff0c..55efd7e7d3b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue @@ -77,6 +77,7 @@ export default { <mr-widget-pipeline :pipeline="pipeline" :pipeline-coverage-delta="mr.pipelineCoverageDelta" + :builds-with-coverage="mr.buildsWithCoverage" :ci-status="mr.ciStatus" :has-ci="mr.hasCI" :pipeline-must-succeed="mr.onlyAllowMergeIfPipelineSucceeds" 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 1b3b589c32f..56a50b55f9d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { s__ } from '~/locale'; export default { 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 936fdc9aff5..a9d148505e1 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 @@ -108,7 +108,9 @@ export default { </div> </template> <div class="row"> - <div class="col-md-5 order-md-last col-12 gl-mt-5 mt-md-n1 pt-md-1 svg-content svg-225"> + <div + class="col-md-5 order-md-last col-12 gl-mt-5 gl-mt-md-n2! gl-pt-md-2 svg-content svg-225" + > <img data-testid="pipeline-image" :src="pipelineSvgPath" /> </div> <div class="col-md-7 order-md-first col-12"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue index c38c41f13b6..ebd2b5cd22d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue @@ -1,10 +1,10 @@ <script> -import { GlTooltipDirective } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui'; export default { components: { - Icon, + GlButton, + GlIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -22,20 +22,26 @@ export default { type: String, required: true, }, + size: { + type: String, + required: false, + default: 'medium', + }, }, }; </script> <template> - <a + <gl-button v-gl-tooltip :title="display.tooltip" :href="link" + :size="size" target="_blank" rel="noopener noreferrer nofollow" :class="cssClass" data-track-event="open_review_app" data-track-label="review_app" > - {{ display.text }} <icon class="fgray" name="external-link" /> - </a> + {{ display.text }} <gl-icon class="fgray" name="external-link" /> + </gl-button> </template> 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 dab0540f44e..859f2c57598 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 @@ -1,5 +1,5 @@ <script> -import { GlSprintf } from '@gitlab/ui'; +import { GlIcon, GlSprintf } from '@gitlab/ui'; import tooltip from '../../vue_shared/directives/tooltip'; import { __ } from '../../locale'; @@ -9,6 +9,7 @@ export default { tooltipTitle: __('A user with write access to the source branch selected this option'), }, components: { + GlIcon, GlSprintf, }, directives: { @@ -26,12 +27,11 @@ export default { </template> </gl-sprintf> </span> - <i + <gl-icon v-tooltip :title="$options.i18n.tooltipTitle" :aria-label="$options.i18n.tooltipTitle" - class="fa fa-question-circle" - > - </i> + name="question-o" + /> </p> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue index d52e6d38ac6..bdcea9871ea 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue @@ -1,13 +1,12 @@ <script> -import { GlDeprecatedButton } from '@gitlab/ui'; +/* eslint-disable vue/no-v-html */ +import { GlButton } from '@gitlab/ui'; import { escape } from 'lodash'; import { __, n__, sprintf, s__ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; export default { components: { - Icon, - GlDeprecatedButton, + GlButton, }, props: { isSquashEnabled: { @@ -80,20 +79,19 @@ export default { class="js-mr-widget-commits-count mr-widget-extension clickable d-flex align-items-center px-3 py-2" @click="toggle()" > - <gl-deprecated-button + <gl-button :aria-label="ariaLabel" - variant="blank" - class="commit-edit-toggle square s24 gl-mr-3" + category="tertiary" + class="commit-edit-toggle gl-mr-3" + :icon="collapseIcon" @click.stop="toggle()" - > - <icon :name="collapseIcon" :size="16" /> - </gl-deprecated-button> + /> <span v-if="expanded">{{ __('Collapse') }}</span> <span v-else> <span class="vertical-align-middle" v-html="message"></span> - <gl-deprecated-button variant="link" class="modify-message-button"> + <gl-button variant="link" class="modify-message-button"> {{ modifyLinkMessage }} - </gl-deprecated-button> + </gl-button> </span> </div> <div v-show="expanded"><slot></slot></div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue index 9df0c045fe4..a5ec095b8ec 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue @@ -1,4 +1,5 @@ <script> +import { GlButton } from '@gitlab/ui'; import { n__ } from '~/locale'; import { stripHtml } from '~/lib/utils/text_utility'; import statusIcon from '../mr_widget_status_icon.vue'; @@ -8,6 +9,7 @@ export default { name: 'MRWidgetFailedToMerge', components: { + GlButton, statusIcon, }, @@ -84,14 +86,14 @@ export default { <span v-else> {{ s__('mrWidget|Merge failed.') }} </span> <span :class="{ 'has-custom-error': mr.mergeError }"> {{ timerText }} </span> </span> - <button - class="btn btn-default btn-sm js-refresh-button" + <gl-button + size="small" + data-testid="merge-request-failed-refresh-button" data-qa-selector="merge_request_error_content" - type="button" @click="refresh" > {{ s__('mrWidget|Refresh now') }} - </button> + </gl-button> </div> </template> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue index 166700dbcbf..58839251edc 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -1,6 +1,6 @@ <script> /* eslint-disable @gitlab/vue-require-i18n-strings */ -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon, GlButton } from '@gitlab/ui'; import { deprecatedCreateFlash as Flash } from '~/flash'; import tooltip from '~/vue_shared/directives/tooltip'; import { s__, __ } from '~/locale'; @@ -19,6 +19,7 @@ export default { statusIcon, ClipboardButton, GlLoadingIcon, + GlButton, }, props: { mr: { @@ -112,48 +113,52 @@ export default { :date-title="mr.metrics.mergedAt" :date-readable="mr.metrics.readableMergedAt" /> - <a + <gl-button v-if="mr.canRevertInCurrentMR" v-tooltip :title="revertTitle" - class="btn btn-close btn-sm" + size="small" + category="secondary" + variant="warning" href="#modal-revert-commit" data-toggle="modal" data-container="body" > {{ revertLabel }} - </a> - <a + </gl-button> + <gl-button v-else-if="mr.revertInForkPath" v-tooltip :href="mr.revertInForkPath" :title="revertTitle" - class="btn btn-close btn-sm" + size="small" + category="secondary" + variant="warning" data-method="post" > {{ revertLabel }} - </a> - <a + </gl-button> + <gl-button v-if="mr.canCherryPickInCurrentMR" v-tooltip :title="cherryPickTitle" - class="btn btn-default btn-sm" + size="small" href="#modal-cherry-pick-commit" data-toggle="modal" data-container="body" > {{ cherryPickLabel }} - </a> - <a + </gl-button> + <gl-button v-else-if="mr.cherryPickInForkPath" v-tooltip :href="mr.cherryPickInForkPath" :title="cherryPickTitle" - class="btn btn-default btn-sm" + size="small" data-method="post" > {{ cherryPickLabel }} - </a> + </gl-button> </div> <section class="mr-info-list" data-qa-selector="merged_status_content"> <p> @@ -181,14 +186,14 @@ export default { </p> <p v-if="shouldShowRemoveSourceBranch" class="space-children"> <span>{{ s__('mrWidget|You can delete the source branch now') }}</span> - <button + <gl-button :disabled="isMakingRequest" - type="button" - class="btn btn-sm btn-default js-remove-branch-button" + size="small" + class="js-remove-branch-button" @click="removeSourceBranch" > {{ s__('mrWidget|Delete source branch') }} - </button> + </gl-button> </p> <p v-if="shouldShowSourceBranchRemoving"> <gl-loading-icon :inline="true" /> 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 8f38ca69453..83783528cc1 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 @@ -1,4 +1,5 @@ <script> +import { GlIcon } from '@gitlab/ui'; import { sprintf, s__ } from '~/locale'; import tooltip from '~/vue_shared/directives/tooltip'; import statusIcon from '../mr_widget_status_icon.vue'; @@ -9,6 +10,7 @@ export default { tooltip, }, components: { + GlIcon, statusIcon, }, props: { @@ -50,7 +52,7 @@ export default { <span class="bold js-branch-text"> <span class="capitalize"> {{ missingBranchName }} </span> {{ s__('mrWidget|branch does not exist.') }} {{ missingBranchNameMessage }} - <i v-tooltip :title="message" :aria-label="message" class="fa fa-question-circle"> </i> + <gl-icon v-tooltip :title="message" :aria-label="message" name="question-o" /> </span> </div> </div> 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 794c994bffe..ec0934c5b4b 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,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { GlLoadingIcon } from '@gitlab/ui'; import { escape } from 'lodash'; import simplePoll from '../../../lib/utils/simple_poll'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue index 4d7d49398eb..14a29483d3c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import emptyStateSVG from 'icons/_mr_widget_empty_state.svg'; export default { 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 930a2b68d8e..240bab58297 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 @@ -1,6 +1,7 @@ <script> +/* eslint-disable vue/no-v-html */ import { isEmpty } from 'lodash'; -import { GlIcon, GlDeprecatedButton, GlSprintf, GlLink } from '@gitlab/ui'; +import { GlIcon, GlButton, GlSprintf, GlLink } from '@gitlab/ui'; import successSvg from 'icons/_icon_status_success.svg'; import warningSvg from 'icons/_icon_status_warning.svg'; import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge'; @@ -37,7 +38,7 @@ export default { GlIcon, GlSprintf, GlLink, - GlDeprecatedButton, + GlButton, MergeTrainHelperText: () => import('ee_component/vue_merge_request_widget/components/merge_train_helper_text.vue'), MergeImmediatelyConfirmationDialog: () => @@ -297,16 +298,16 @@ export default { <div class="media-body"> <div class="mr-widget-body-controls media space-children"> <span class="btn-group"> - <gl-deprecated-button - size="sm" + <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 > - {{ mergeButtonText }} - </gl-deprecated-button> <button v-if="shouldShowMergeImmediatelyDropdown" :disabled="isMergeButtonDisabled" 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 3cf7dc3c4d1..6608381f348 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue @@ -1,11 +1,11 @@ <script> -import Icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import tooltip from '~/vue_shared/directives/tooltip'; import { __ } from '~/locale'; export default { components: { - Icon, + GlIcon, }, directives: { tooltip, @@ -62,7 +62,7 @@ export default { rel="noopener noreferrer nofollow" data-container="body" > - <icon name="question" /> + <gl-icon name="question" /> </a> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue index 44668170fe4..61cc950f058 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 @@ -55,13 +55,15 @@ export default { }, methods: { removeWipMutation() { + const { mergeRequestQueryVariables } = this; + this.isMakingRequest = true; this.$apollo .mutate({ mutation: removeWipMutation, variables: { - ...this.mergeRequestQueryVariables, + ...mergeRequestQueryVariables, wip: false, }, update( @@ -83,14 +85,14 @@ export default { const data = store.readQuery({ query: getStateQuery, - variables: this.mergeRequestQueryVariables, + variables: mergeRequestQueryVariables, }); data.project.mergeRequest.workInProgress = workInProgress; data.project.mergeRequest.title = title; store.writeQuery({ query: getStateQuery, data, - variables: this.mergeRequestQueryVariables, + variables: mergeRequestQueryVariables, }); }, optimisticResponse: { 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 c7d9453a5c9..4de41dd5887 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 @@ -1,5 +1,5 @@ <script> -import { GlSkeletonLoading, GlSprintf } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf } from '@gitlab/ui'; import { n__ } from '~/locale'; import axios from '~/lib/utils/axios_utils'; import MrWidgetExpanableSection from '../mr_widget_expandable_section.vue'; 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 36a883869f1..43ce748b41d 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 @@ -7,6 +7,7 @@ import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps'; import { sprintf, s__, __ } from '~/locale'; import Project from '~/pages/projects/project'; import SmartInterval from '~/smart_interval'; +import { secondsToMilliseconds } from '~/lib/utils/datetime_utility'; import { deprecatedCreateFlash as createFlash } from '../flash'; import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables'; import Loading from './components/loading.vue'; @@ -96,12 +97,11 @@ export default { variables() { return this.mergeRequestQueryVariables; }, - result({ - data: { - project: { mergeRequest }, - }, - }) { - this.mr.setGraphqlData(mergeRequest); + result({ data: { project } }) { + if (project) { + this.mr.setGraphqlData(project); + this.loading = false; + } }, }, }, @@ -120,9 +120,17 @@ export default { mr: store, state: store && store.state, service: store && this.createService(store), + loading: true, }; }, computed: { + isLoaded() { + if (window.gon?.features?.mergeRequestWidgetGraphql) { + return !this.loading; + } + + return this.mr; + }, shouldRenderApprovals() { return this.mr.state !== 'nothingToMerge'; }, @@ -195,7 +203,10 @@ export default { }, mounted() { MRWidgetService.fetchInitialData() - .then(({ data }) => this.initWidget(data)) + .then(({ data, headers }) => { + this.startingPollInterval = Number(headers['POLL-INTERVAL']); + this.initWidget(data); + }) .catch(() => createFlash(__('Unable to load the merge request widget. Try reloading the page.')), ); @@ -285,9 +296,10 @@ export default { initPolling() { this.pollingInterval = new SmartInterval({ callback: this.checkStatus, - startingInterval: 10 * 1000, - maxInterval: 240 * 1000, - hiddenInterval: window.gon?.features?.widgetVisibilityPolling && 360 * 1000, + startingInterval: this.startingPollInterval, + maxInterval: this.startingPollInterval + secondsToMilliseconds(4 * 60), + hiddenInterval: + window.gon?.features?.widgetVisibilityPolling && secondsToMilliseconds(6 * 60), incrementByFactorOf: 2, }); }, @@ -409,7 +421,7 @@ export default { }; </script> <template> - <div v-if="mr" class="mr-state-widget gl-mt-3"> + <div v-if="isLoaded" class="mr-state-widget gl-mt-3"> <mr-widget-header :mr="mr" /> <mr-widget-suggest-pipeline v-if="shouldSuggestPipelines" diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql index 488397e7735..44fc1cc7f23 100644 --- a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql +++ b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql @@ -1,7 +1,27 @@ query getState($projectPath: ID!, $iid: String!) { project(fullPath: $projectPath) { + archived + onlyAllowMergeIfPipelineSucceeds + mergeRequest(iid: $iid) { - title + autoMergeEnabled + commitCount + conflicts + diffHeadSha + mergeError + mergeStatus + mergeableDiscussionsState + pipelines(first: 1) { + nodes { + status + } + } + shouldBeRebased + sourceBranchExists + targetBranchExists + userPermissions { + canMerge + } workInProgress } } 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 ee9e3cc6d08..2ad15f231bb 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 @@ -1,3 +1,4 @@ +import { normalizeHeaders } from '~/lib/utils/common_utils'; import axios from '../../lib/utils/axios_utils'; export default class MRWidgetService { @@ -82,6 +83,11 @@ export default class MRWidgetService { return Promise.all([ axios.get(window.gl.mrWidgetData.merge_request_cached_widget_path), axios.get(window.gl.mrWidgetData.merge_request_widget_path), - ]).then(axios.spread((res, cachedRes) => ({ data: Object.assign(res.data, cachedRes.data) }))); + ]).then( + axios.spread((res, cachedRes) => ({ + data: Object.assign(res.data, cachedRes.data), + headers: normalizeHeaders(res.headers), + })), + ); } } 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 5215d210e1c..15d67ea18ea 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,5 @@ import { s__, n__ } from '~/locale'; -// eslint-disable-next-line import/prefer-default-export export const title = state => { if (state.isLoading) { return s__('BuildArtifacts|Loading artifacts'); diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js index 3bd512c89bf..9d3f4eb01ed 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js @@ -17,7 +17,7 @@ export default function deviseState() { return stateKey.pipelineFailed; } else if (this.workInProgress) { return stateKey.workInProgress; - } else if (this.hasMergeableDiscussionsState) { + } else if (this.hasMergeableDiscussionsState && !this.autoMergeEnabled) { return stateKey.unresolvedDiscussions; } else if (this.isPipelineBlocked) { return stateKey.pipelineBlocked; 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 8c98ba1b023..846b1c453a1 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 @@ -43,15 +43,14 @@ export default class MergeRequestStore { this.conflictsDocsPath = data.conflicts_docs_path; this.mergeRequestPipelinesHelpPath = data.merge_request_pipelines_docs_path; this.mergeTrainWhenPipelineSucceedsDocsPath = data.merge_train_when_pipeline_succeeds_docs_path; - this.mergeStatus = data.merge_status; this.commitMessage = data.default_merge_commit_message; - this.shortMergeCommitSha = data.short_merge_commit_sha; - this.mergeCommitSha = data.merge_commit_sha; + this.shortMergeCommitSha = data.short_merged_commit_sha; + this.mergeCommitSha = data.merged_commit_sha; this.commitMessageWithDescription = data.default_merge_commit_message_with_description; - this.commitsCount = data.commits_count; this.divergedCommitsCount = data.diverged_commits_count; this.pipeline = data.pipeline || {}; this.pipelineCoverageDelta = data.pipeline_coverage_delta; + this.buildsWithCoverage = data.builds_with_coverage; this.mergePipeline = data.merge_pipeline || {}; this.deployments = this.deployments || data.deployments || []; this.postMergeDeployments = this.postMergeDeployments || []; @@ -60,9 +59,6 @@ export default class MergeRequestStore { this.rebaseInProgress = data.rebase_in_progress; this.mergeRequestDiffsPath = data.diffs_path; this.approvalsWidgetType = data.approvals_widget_type; - this.projectArchived = data.project_archived; - this.branchMissing = data.branch_missing; - this.hasConflicts = data.has_conflicts; if (data.issues_links) { const links = data.issues_links; @@ -80,25 +76,18 @@ export default class MergeRequestStore { this.setToAutoMergeBy = MergeRequestStore.formatUserObject(data.merge_user || {}); this.mergeUserId = data.merge_user_id; this.currentUserId = gon.current_user_id; - this.mergeError = data.merge_error; this.sourceBranchRemoved = !data.source_branch_exists; this.shouldRemoveSourceBranch = data.remove_source_branch || false; - this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false; - this.autoMergeEnabled = Boolean(data.auto_merge_enabled); this.autoMergeStrategy = data.auto_merge_strategy; this.availableAutoMergeStrategies = data.available_auto_merge_strategies; this.preferredAutoMergeStrategy = MergeRequestStore.getPreferredAutoMergeStrategy( this.availableAutoMergeStrategies, ); this.ffOnlyEnabled = data.ff_only_enabled; - this.shouldBeRebased = Boolean(data.should_be_rebased); this.isRemovingSourceBranch = this.isRemovingSourceBranch || false; this.mergeRequestState = data.state; this.isOpen = this.mergeRequestState === 'opened'; - this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false; - this.isSHAMismatch = this.sha !== data.diff_head_sha; this.latestSHA = data.diff_head_sha; - this.canBeMerged = data.can_be_merged || false; this.isMergeAllowed = data.mergeable || false; this.mergeOngoing = data.merge_ongoing; this.allowCollaboration = data.allow_collaboration; @@ -108,13 +97,13 @@ export default class MergeRequestStore { // CI related this.hasCI = data.has_ci; this.ciStatus = data.ci_status; - this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled'; this.isPipelinePassing = this.ciStatus === 'success' || this.ciStatus === 'success-with-warnings'; this.isPipelineSkipped = this.ciStatus === 'skipped'; this.pipelineDetailedStatus = pipelineStatus; this.isPipelineActive = data.pipeline ? data.pipeline.active : false; - this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false; + this.isPipelineBlocked = + data.only_allow_merge_if_pipeline_succeeds && pipelineStatus?.group === 'manual'; this.ciStatusFaviconPath = pipelineStatus ? pipelineStatus.favicon : null; this.terraformReportsPath = data.terraform_reports_path; this.testResultsPath = data.test_reports_path; @@ -133,11 +122,24 @@ export default class MergeRequestStore { this.removeWIPPath = data.remove_wip_path; this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path; this.mergePath = data.merge_path; - this.canMerge = Boolean(data.merge_path); - this.mergeCommitPath = data.merge_commit_path; + this.mergeCommitPath = data.merged_commit_path; this.canPushToSourceBranch = data.can_push_to_source_branch; - if (data.work_in_progress !== undefined) { + if (!window.gon?.features?.mergeRequestWidgetGraphql) { + this.autoMergeEnabled = Boolean(data.auto_merge_enabled); + this.canBeMerged = data.can_be_merged || false; + this.canMerge = Boolean(data.merge_path); + this.commitsCount = data.commits_count; + this.branchMissing = data.branch_missing; + this.hasConflicts = data.has_conflicts; + this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false; + this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled'; + this.mergeError = data.merge_error; + this.mergeStatus = data.merge_status; + this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false; + this.projectArchived = data.project_archived; + this.isSHAMismatch = this.sha !== data.diff_head_sha; + this.shouldBeRebased = Boolean(data.should_be_rebased); this.workInProgress = data.work_in_progress; } @@ -154,8 +156,27 @@ export default class MergeRequestStore { this.setState(); } - setGraphqlData(data) { - this.workInProgress = data.workInProgress; + setGraphqlData(project) { + const { mergeRequest } = project; + const pipeline = mergeRequest.pipelines?.nodes?.[0]; + + this.projectArchived = project.archived; + this.onlyAllowMergeIfPipelineSucceeds = project.onlyAllowMergeIfPipelineSucceeds; + + this.autoMergeEnabled = mergeRequest.autoMergeEnabled; + this.canBeMerged = mergeRequest.mergeStatus === 'can_be_merged'; + this.canMerge = mergeRequest.userPermissions.canMerge; + this.ciStatus = pipeline?.status.toLowerCase(); + this.commitsCount = mergeRequest.commitCount; + this.branchMissing = !mergeRequest.sourceBranchExists || !mergeRequest.targetBranchExists; + this.hasConflicts = mergeRequest.conflicts; + this.hasMergeableDiscussionsState = mergeRequest.mergeableDiscussionsState === false; + this.mergeError = mergeRequest.mergeError; + this.mergeStatus = mergeRequest.mergeStatus; + this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled'; + this.isSHAMismatch = this.sha !== mergeRequest.diffHeadSha; + this.shouldBeRebased = mergeRequest.shouldBeRebased; + this.workInProgress = mergeRequest.workInProgress; this.setState(); } diff --git a/app/assets/javascripts/vue_shared/components/actions_button.vue b/app/assets/javascripts/vue_shared/components/actions_button.vue new file mode 100644 index 00000000000..f333ab49ead --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/actions_button.vue @@ -0,0 +1,90 @@ +<script> +import { + GlDropdown, + GlDropdownItem, + GlDropdownDivider, + GlLink, + GlTooltipDirective, +} from '@gitlab/ui'; + +export default { + components: { + GlDropdown, + GlDropdownItem, + GlDropdownDivider, + GlLink, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + actions: { + type: Array, + required: true, + }, + selectedKey: { + type: String, + required: false, + default: '', + }, + }, + computed: { + hasMultipleActions() { + return this.actions.length > 1; + }, + selectedAction() { + return this.actions.find(x => x.key === this.selectedKey) || this.actions[0]; + }, + }, + methods: { + handleItemClick(action) { + this.$emit('select', action.key); + }, + handleClick(action, evt) { + return action.handle?.(evt); + }, + }, +}; +</script> + +<template> + <gl-dropdown + v-if="hasMultipleActions" + v-gl-tooltip="selectedAction.tooltip" + class="gl-button-deprecated-adapter" + :text="selectedAction.text" + :split-href="selectedAction.href" + split + @click="handleClick(selectedAction, $event)" + > + <template slot="button-content"> + <span class="gl-new-dropdown-button-text" v-bind="selectedAction.attrs"> + {{ selectedAction.text }} + </span> + </template> + <template v-for="(action, index) in actions"> + <gl-dropdown-item + :key="action.key" + class="gl-dropdown-item-deprecated-adapter" + :is-check-item="true" + :is-checked="action.key === selectedAction.key" + :secondary-text="action.secondaryText" + :data-testid="`action_${action.key}`" + @click="handleItemClick(action)" + > + {{ action.text }} + </gl-dropdown-item> + <gl-dropdown-divider v-if="index != actions.length - 1" :key="action.key + '_divider'" /> + </template> + </gl-dropdown> + <gl-link + v-else-if="selectedAction" + v-gl-tooltip="selectedAction.tooltip" + v-bind="selectedAction.attrs" + class="btn" + :href="selectedAction.href" + @click="handleClick(selectedAction, $event)" + > + {{ selectedAction.text }} + </gl-link> +</template> diff --git a/app/assets/javascripts/vue_shared/components/alert_details_table.vue b/app/assets/javascripts/vue_shared/components/alert_details_table.vue new file mode 100644 index 00000000000..c94e784c01e --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/alert_details_table.vue @@ -0,0 +1,70 @@ +<script> +import { GlLoadingIcon, GlTable } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { + capitalizeFirstCharacter, + convertToSentenceCase, + splitCamelCase, +} from '~/lib/utils/text_utility'; + +const thClass = 'gl-bg-transparent! gl-border-1! gl-border-b-solid! gl-border-gray-200!'; +const tdClass = 'gl-border-gray-100! gl-p-5!'; + +export default { + components: { + GlLoadingIcon, + GlTable, + }, + props: { + alert: { + type: Object, + required: false, + default: null, + }, + loading: { + type: Boolean, + required: true, + }, + }, + fields: [ + { + key: 'fieldName', + label: s__('AlertManagement|Key'), + thClass, + tdClass, + formatter: string => capitalizeFirstCharacter(convertToSentenceCase(splitCamelCase(string))), + }, + { + key: 'value', + thClass: `${thClass} w-60p`, + tdClass, + label: s__('AlertManagement|Value'), + }, + ], + computed: { + items() { + if (!this.alert) { + return []; + } + return Object.entries(this.alert).map(([fieldName, value]) => ({ + fieldName, + value, + })); + }, + }, +}; +</script> +<template> + <gl-table + class="alert-management-details-table" + :busy="loading" + :empty-text="s__('AlertManagement|No alert data to display.')" + :items="items" + :fields="$options.fields" + show-empty + > + <template #table-busy> + <gl-loading-icon size="lg" color="dark" class="gl-mt-5" /> + </template> + </gl-table> +</template> diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue index c0a42e08dee..e1f54b62223 100644 --- a/app/assets/javascripts/vue_shared/components/awards_list.vue +++ b/app/assets/javascripts/vue_shared/components/awards_list.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { groupBy } from 'lodash'; import { GlIcon } from '@gitlab/ui'; import tooltip from '~/vue_shared/directives/tooltip'; diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue index 52ce05f0d99..d0f5570db6b 100644 --- a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue @@ -1,4 +1,5 @@ <script> +import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue'; import ViewerMixin from './mixins'; import { handleBlobRichViewer } from '~/blob/viewer'; @@ -7,6 +8,9 @@ export default { components: { MarkdownFieldView, }, + directives: { + SafeHtml, + }, mixins: [ViewerMixin], mounted() { handleBlobRichViewer(this.$refs.content, this.type); @@ -14,5 +18,5 @@ export default { }; </script> <template> - <markdown-field-view ref="content" v-html="content" /> + <markdown-field-view ref="content" v-safe-html="content" /> </template> diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue index 55a6267f9ff..bbe72a2b122 100644 --- a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { GlIcon } from '@gitlab/ui'; import ViewerMixin from './mixins'; import { HIGHLIGHT_CLASS_NAME } from './constants'; 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 7431b7e9ed4..f28e49df56e 100644 --- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue @@ -1,12 +1,11 @@ <script> -import { GlTooltipDirective } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; import getCommitIconMap from '~/ide/commit_icon'; import { __ } from '~/locale'; export default { components: { - Icon, + GlIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -81,7 +80,7 @@ export default { :class="{ 'ml-auto': isCentered }" class="file-changed-icon d-inline-block" > - <icon v-if="showIcon" :name="changedIcon" :size="size" :class="changedIconClass" /> + <gl-icon v-if="showIcon" :name="changedIcon" :size="size" :class="changedIconClass" /> </span> </template> diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue index 0e0bb8735b4..d7af3b3298e 100644 --- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue +++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue @@ -39,6 +39,11 @@ export default { required: false, default: true, }, + iconClasses: { + type: String, + required: false, + default: '', + }, }, computed: { cssClass() { @@ -55,7 +60,7 @@ export default { :class="cssClass" :title="!showText ? status.text : ''" > - <ci-icon :status="status" /> + <ci-icon :status="status" :css-classes="iconClasses" /> <template v-if="showText"> {{ status.text }} diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index 890dbe86c0d..ff665d9cc58 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -1,5 +1,5 @@ <script> -import Icon from './icon.vue'; +import { GlIcon } from '@gitlab/ui'; /** * Renders CI icon based on API response shared between all places where it is used. @@ -28,7 +28,7 @@ const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; export default { components: { - Icon, + GlIcon, }, props: { status: { @@ -66,5 +66,5 @@ export default { }; </script> <template> - <span :class="cssClass"> <icon :name="icon" :size="size" :class="cssClasses" /> </span> + <span :class="cssClass"> <gl-icon :name="icon" :size="size" :class="cssClasses" /> </span> </template> diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue index 6f5ea8dcbee..5c6bd5892ae 100644 --- a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue @@ -1,7 +1,7 @@ <script> import { - GlNewDropdown, - GlNewDropdownHeader, + GlDropdown, + GlDropdownSectionHeader, GlFormInputGroup, GlButton, GlTooltipDirective, @@ -11,8 +11,8 @@ import { getHTTPProtocol } from '~/lib/utils/url_utility'; export default { components: { - GlNewDropdown, - GlNewDropdownHeader, + GlDropdown, + GlDropdownSectionHeader, GlFormInputGroup, GlButton, }, @@ -45,10 +45,10 @@ export default { }; </script> <template> - <gl-new-dropdown right :text="$options.labels.defaultLabel" category="primary" variant="info"> + <gl-dropdown right :text="$options.labels.defaultLabel" category="primary" variant="info"> <div class="pb-2 mx-1"> <template v-if="sshLink"> - <gl-new-dropdown-header>{{ $options.labels.ssh }}</gl-new-dropdown-header> + <gl-dropdown-section-header>{{ $options.labels.ssh }}</gl-dropdown-section-header> <div class="mx-3"> <gl-form-input-group :value="sshLink" readonly select-on-click> @@ -67,7 +67,7 @@ export default { </template> <template v-if="httpLink"> - <gl-new-dropdown-header>{{ httpLabel }}</gl-new-dropdown-header> + <gl-dropdown-section-header>{{ httpLabel }}</gl-dropdown-section-header> <div class="mx-3"> <gl-form-input-group :value="httpLink" readonly select-on-click> @@ -85,5 +85,5 @@ export default { </div> </template> </div> - </gl-new-dropdown> + </gl-dropdown> </template> diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue index 23bea6c28b4..c1c8fb3a6e2 100644 --- a/app/assets/javascripts/vue_shared/components/commit.vue +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -1,10 +1,9 @@ <script> import { isString, isEmpty } from 'lodash'; -import { GlTooltipDirective, GlLink } from '@gitlab/ui'; +import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import UserAvatarLink from './user_avatar/user_avatar_link.vue'; -import Icon from './icon.vue'; export default { directives: { @@ -12,14 +11,14 @@ export default { }, components: { UserAvatarLink, - Icon, + GlIcon, GlLink, TooltipOnTruncate, }, props: { /** * Indicates the existence of a tag. - * Used to render the correct icon, if true will render `fa-tag` icon, + * Used to render the correct GlIcon, if true will render `tag` GlIcon, * if false will render a svg sprite fork icon */ tag: { @@ -141,9 +140,9 @@ export default { <div class="branch-commit cgray"> <template v-if="shouldShowRefInfo"> <div class="icon-container"> - <icon v-if="tag" name="tag" /> - <icon v-else-if="mergeRequestRef" name="git-merge" /> - <icon v-else name="branch" /> + <gl-icon v-if="tag" name="tag" /> + <gl-icon v-else-if="mergeRequestRef" name="git-merge" /> + <gl-icon v-else name="branch" /> </div> <gl-link @@ -163,7 +162,7 @@ export default { >{{ commitRef.name }}</gl-link > </template> - <icon name="commit" class="commit-icon js-commit-icon" /> + <gl-icon name="commit" class="commit-icon js-commit-icon" /> <gl-link :href="commitUrl" class="commit-sha mr-0">{{ shortSha }}</gl-link> 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 3bf629d4acb..f9d3d76e7f5 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 @@ -1,12 +1,11 @@ <script> -import { GlLink } from '@gitlab/ui'; -import Icon from '../../icon.vue'; +import { GlLink, GlIcon } from '@gitlab/ui'; import { numberToHumanSize } from '../../../../lib/utils/number_utils'; export default { components: { GlLink, - Icon, + GlIcon, }, props: { path: { @@ -52,7 +51,7 @@ export default { :download="fileName" target="_blank" > - <icon :size="16" name="download" class="float-left gl-mr-3" /> + <gl-icon :size="16" name="download" class="float-left gl-mr-3" /> {{ __('Download') }} </gl-link> </div> 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 f9b678e33cd..6bb05e59f6b 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue @@ -1,8 +1,9 @@ <script> +/* eslint-disable vue/no-v-html */ import $ from 'jquery'; import '~/behaviors/markdown/render_gfm'; -import { GlSkeletonLoading } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import { forEach, escape } from 'lodash'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; 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 3b6b0a91e97..a7e6438a935 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 @@ -1,11 +1,5 @@ <script> -import { - GlIcon, - GlButton, - GlDeprecatedDropdown, - GlDeprecatedDropdownItem, - GlFormGroup, -} from '@gitlab/ui'; +import { GlIcon, GlButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import { convertToFixedRange, isEqualTimeRanges, findTimeRange } from '~/lib/utils/datetime_range'; @@ -29,8 +23,8 @@ export default { components: { GlIcon, GlButton, - GlDeprecatedDropdown, - GlDeprecatedDropdownItem, + GlDropdown, + GlDropdownItem, GlFormGroup, TooltipOnTruncate, DateTimePickerInput, @@ -212,7 +206,7 @@ export default { placement="top" class="d-inline-block" > - <gl-deprecated-dropdown + <gl-dropdown ref="dropdown" :text="timeWindowText" v-bind="$attrs" @@ -228,15 +222,15 @@ export default { <gl-icon class="gl-dropdown-caret" name="chevron-down" aria-hidden="true" /> </template> - <div class="d-flex justify-content-between gl-p-2-deprecated-no-really-do-not-use-me"> + <div class="d-flex justify-content-between gl-p-2"> <gl-form-group v-if="customEnabled" :label="customLabel" label-for="custom-from-time" - label-class="gl-pb-1-deprecated-no-really-do-not-use-me" - class="custom-time-range-form-group col-md-7 gl-pl-1-deprecated-no-really-do-not-use-me gl-pr-0 m-0" + label-class="gl-pb-2" + class="custom-time-range-form-group col-md-7 gl-pl-2 gl-pr-0 m-0" > - <div class="gl-pt-2-deprecated-no-really-do-not-use-me"> + <div class="gl-pt-3"> <date-time-picker-input id="custom-time-from" v-model="startInput" @@ -264,15 +258,12 @@ export default { </gl-button> </gl-form-group> </gl-form-group> - <gl-form-group - label-for="group-id-dropdown" - class="col-md-5 gl-pl-1-deprecated-no-really-do-not-use-me gl-pr-1-deprecated-no-really-do-not-use-me m-0" - > + <gl-form-group label-for="group-id-dropdown" class="col-md-5 gl-px-2 m-0"> <template #label> - <span class="gl-pl-5-deprecated-no-really-do-not-use-me">{{ __('Quick range') }}</span> + <span class="gl-pl-7">{{ __('Quick range') }}</span> </template> - <gl-deprecated-dropdown-item + <gl-dropdown-item v-for="(option, index) in options" :key="index" data-qa-selector="quick_range_item" @@ -286,9 +277,9 @@ export default { :class="{ invisible: !isOptionActive(option) }" /> {{ option.label }} - </gl-deprecated-dropdown-item> + </gl-dropdown-item> </gl-form-group> </div> - </gl-deprecated-dropdown> + </gl-dropdown> </tooltip-on-truncate> </template> diff --git a/app/assets/javascripts/vue_shared/components/dismissible_alert.vue b/app/assets/javascripts/vue_shared/components/dismissible_alert.vue index 986fa14349e..8494f99fd7d 100644 --- a/app/assets/javascripts/vue_shared/components/dismissible_alert.vue +++ b/app/assets/javascripts/vue_shared/components/dismissible_alert.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { GlAlert } from '@gitlab/ui'; export default { diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue index 610bce9a705..7157337f8f3 100644 --- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue +++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue @@ -41,12 +41,5 @@ export default { autocomplete="off" /> <i class="fa fa-search dropdown-input-search" aria-hidden="true" data-hidden="true"> </i> - <i - class="fa fa-times dropdown-input-clear js-dropdown-input-clear" - aria-hidden="true" - data-hidden="true" - role="button" - > - </i> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue b/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue index e1f336f5250..4d85726065b 100644 --- a/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue +++ b/app/assets/javascripts/vue_shared/components/droplab_dropdown_button.vue @@ -1,10 +1,9 @@ <script> -import { GlDeprecatedButton } from '@gitlab/ui'; -import Icon from './icon.vue'; +import { GlDeprecatedButton, GlIcon } from '@gitlab/ui'; export default { components: { - Icon, + GlIcon, GlDeprecatedButton, }, props: { @@ -73,7 +72,7 @@ export default { data-display="static" data-toggle="dropdown" > - <icon name="chevron-down" :aria-label="__('toggle dropdown')" /> + <gl-icon name="chevron-down" :aria-label="__('toggle dropdown')" /> </button> <ul :class="dropdownClass" class="dropdown-menu dropdown-open-top"> <template v-for="(action, index) in actions"> 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 d6f591ccca1..012aca8105a 100644 --- a/app/assets/javascripts/vue_shared/components/file_finder/index.vue +++ b/app/assets/javascripts/vue_shared/components/file_finder/index.vue @@ -2,6 +2,7 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus'; import Mousetrap from 'mousetrap'; import VirtualList from 'vue-virtual-scroll-list'; +import { GlIcon } from '@gitlab/ui'; import Item from './item.vue'; import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; @@ -9,10 +10,11 @@ export const MAX_FILE_FINDER_RESULTS = 40; export const FILE_FINDER_ROW_HEIGHT = 55; export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33; -const originalStopCallback = Mousetrap.stopCallback; +const originalStopCallback = Mousetrap.prototype.stopCallback; export default { components: { + GlIcon, Item, VirtualList, }, @@ -126,7 +128,7 @@ export default { this.focusedIndex = 0; } - Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => { + Mousetrap.bind(['t', 'mod+p'], e => { if (e.preventDefault) { e.preventDefault(); } @@ -134,7 +136,18 @@ export default { this.toggle(!this.visible); }); - Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo); + Mousetrap.prototype.stopCallback = function customStopCallback(e, el, combo) { + if ( + (combo === 't' && el.classList.contains('dropdown-input-field')) || + el.classList.contains('inputarea') + ) { + return true; + } else if (combo === 'mod+p') { + return false; + } + + return originalStopCallback.call(this, e, el, combo); + }; }, methods: { toggle(visible) { @@ -199,18 +212,6 @@ export default { this.cancelMouseOver = false; this.onMouseOver(index); }, - mousetrapStopCallback(e, el, combo) { - if ( - (combo === 't' && el.classList.contains('dropdown-input-field')) || - el.classList.contains('inputarea') - ) { - return true; - } else if (combo === 'command+p' || combo === 'ctrl+p') { - return false; - } - - return originalStopCallback(e, el, combo); - }, }, }; </script> @@ -236,12 +237,13 @@ export default { aria-hidden="true" class="fa fa-search dropdown-input-search" ></i> - <i - :aria-label="__('Clear search input')" + <gl-icon + name="close" + class="dropdown-input-clear" role="button" - class="fa fa-times dropdown-input-clear" + :aria-label="__('Clear search input')" @click="clearSearchInput" - ></i> + /> </div> <div> <virtual-list ref="virtualScrollList" :size="listHeight" :remain="listShowCount" wtag="ul"> diff --git a/app/assets/javascripts/vue_shared/components/file_finder/item.vue b/app/assets/javascripts/vue_shared/components/file_finder/item.vue index 79c62cd9938..4c496ba3f9b 100644 --- a/app/assets/javascripts/vue_shared/components/file_finder/item.vue +++ b/app/assets/javascripts/vue_shared/components/file_finder/item.vue @@ -1,6 +1,6 @@ <script> import fuzzaldrinPlus from 'fuzzaldrin-plus'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import FileIcon from '../file_icon.vue'; import ChangedFileIcon from '../changed_file_icon.vue'; @@ -8,7 +8,7 @@ const MAX_PATH_LENGTH = 60; export default { components: { - Icon, + GlIcon, ChangedFileIcon, FileIcon, }, @@ -103,10 +103,10 @@ export default { <span v-if="file.changed || file.tempFile" v-once class="diff-changed-stats"> <span v-if="showDiffStats"> <span class="cgreen bold"> - <icon name="file-addition" class="align-text-top" /> {{ file.addedLines }} + <gl-icon name="file-addition" class="align-text-top" /> {{ file.addedLines }} </span> <span class="cred bold ml-1"> - <icon name="file-deletion" class="align-text-top" /> {{ file.removedLines }} + <gl-icon name="file-deletion" class="align-text-top" /> {{ file.removedLines }} </span> </span> <changed-file-icon v-else :file="file" /> diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue index 0952e37e46e..c1c4f437dee 100644 --- a/app/assets/javascripts/vue_shared/components/file_row.vue +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -14,10 +14,20 @@ export default { type: Object, required: true, }, + fileUrl: { + type: String, + required: false, + default: '', + }, level: { type: Number, required: true, }, + fileClasses: { + type: String, + required: false, + default: '', + }, }, computed: { isTree() { @@ -43,6 +53,9 @@ export default { // don't output a title if we don't have the expanded path return this.file?.tree?.length ? this.file.tree[0].parentPath : false; }, + fileRouterUrl() { + return this.fileUrl || `/project${this.file.url}`; + }, }, watch: { 'file.active': function fileActiveWatch(active) { @@ -69,7 +82,7 @@ export default { this.toggleTreeOpen(this.file.path); } - if (this.$router) this.$router.push(`/project${this.file.url}`); + if (this.$router && !this.hasUrlAtCurrentRoute()) this.$router.push(this.fileRouterUrl); if (this.isBlob) this.clickedFile(this.file.path); }, @@ -99,7 +112,7 @@ export default { hasUrlAtCurrentRoute() { if (!this.$router || !this.$router.currentRoute) return true; - return this.$router.currentRoute.path === `/project${escapeFileUrl(this.file.url)}`; + return this.$router.currentRoute.path === escapeFileUrl(this.fileRouterUrl); }, }, }; @@ -123,6 +136,7 @@ export default { :style="levelIndentation" class="file-row-name str-truncated" data-qa-selector="file_name_content" + :class="fileClasses" > <file-icon class="file-row-icon" diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js index 7b3d1d0afd6..3d8afd162cb 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js @@ -1,8 +1,11 @@ +/* eslint-disable @gitlab/require-i18n-strings */ import { __ } from '~/locale'; -export const ANY_AUTHOR = 'Any'; +const DEFAULT_LABEL_NO_LABEL = { value: 'No label', text: __('No label') }; +export const DEFAULT_LABEL_NONE = { value: 'None', text: __('None') }; +export const DEFAULT_LABEL_ANY = { value: 'Any', text: __('Any') }; -export const NO_LABEL = 'No label'; +export const DEFAULT_LABELS = [DEFAULT_LABEL_NO_LABEL]; export const DEBOUNCE_DELAY = 200; @@ -11,13 +14,11 @@ export const SortDirection = { ascending: 'ascending', }; -export const defaultMilestones = [ - // eslint-disable-next-line @gitlab/require-i18n-strings - { value: 'None', text: __('None') }, - // eslint-disable-next-line @gitlab/require-i18n-strings - { value: 'Any', text: __('Any') }, - // eslint-disable-next-line @gitlab/require-i18n-strings +export const DEFAULT_MILESTONES = [ + DEFAULT_LABEL_NONE, + DEFAULT_LABEL_ANY, { value: 'Upcoming', text: __('Upcoming') }, - // eslint-disable-next-line @gitlab/require-i18n-strings { value: 'Started', text: __('Started') }, ]; + +/* eslint-enable @gitlab/require-i18n-strings */ 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 ee293d37b66..25478ad6f4f 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 @@ -3,8 +3,8 @@ import { GlFilteredSearch, GlButtonGroup, GlButton, - GlNewDropdown as GlDropdown, - GlNewDropdownItem as GlDropdownItem, + GlDropdown, + GlDropdownItem, GlTooltipDirective, } from '@gitlab/ui'; @@ -15,7 +15,7 @@ import { deprecatedCreateFlash as createFlash } from '~/flash'; import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store'; import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; -import { stripQuotes } from './filtered_search_utils'; +import { stripQuotes, uniqueTokens } from './filtered_search_utils'; import { SortDirection } from './constants'; export default { @@ -120,10 +120,31 @@ export default { ? __('Sort direction: Ascending') : __('Sort direction: Descending'); }, + /** + * This prop fixes a behaviour affecting GlFilteredSearch + * where selecting duplicate token values leads to history + * dropdown also showing that selection. + */ filteredRecentSearches() { - return this.recentSearchesStorageKey - ? this.recentSearches.filter(item => typeof item !== 'string') - : undefined; + if (this.recentSearchesStorageKey) { + const knownItems = []; + return this.recentSearches.reduce((historyItems, item) => { + // Only include non-string history items (discard items from legacy search) + if (typeof item !== 'string') { + const sanitizedItem = uniqueTokens(item); + const itemString = JSON.stringify(sanitizedItem); + // Only include items which aren't already part of history + if (!knownItems.includes(itemString)) { + historyItems.push(sanitizedItem); + // We're storing string for comparision as doing direct object compare + // won't work due to object reference not being the same. + knownItems.push(itemString); + } + } + return historyItems; + }, []); + } + return undefined; }, }, watch: { @@ -245,12 +266,14 @@ export default { this.recentSearchesService.save(resultantSearches); this.recentSearches = []; }, - handleFilterSubmit(filters) { + handleFilterSubmit() { + const filterTokens = uniqueTokens(this.filterValue); + this.filterValue = filterTokens; if (this.recentSearchesStorageKey) { this.recentSearchesPromise .then(() => { - if (filters.length) { - const resultantSearches = this.recentSearchesStore.addRecentSearch(filters); + if (filterTokens.length) { + const resultantSearches = this.recentSearchesStore.addRecentSearch(filterTokens); this.recentSearchesService.save(resultantSearches); this.recentSearches = resultantSearches; } @@ -260,7 +283,7 @@ export default { }); } this.blurSearchInput(); - this.$emit('onFilter', this.removeQuotesEnclosure(filters)); + this.$emit('onFilter', this.removeQuotesEnclosure(filterTokens)); }, }, }; 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 85f7f746b49..e7d7b7d9f1b 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 @@ -1,4 +1,164 @@ -// eslint-disable-next-line import/prefer-default-export -export const stripQuotes = value => { - return value.includes(' ') ? value.slice(1, -1) : value; +import { isEmpty } from 'lodash'; +import { queryToObject } from '~/lib/utils/url_utility'; + +/** + * Strips enclosing quotations from a string if it has one. + * + * @param {String} value String to strip quotes from + * + * @returns {String} String without any enclosure + */ +export const stripQuotes = value => value.replace(/^('|")(.*)('|")$/, '$2'); + +/** + * This method removes duplicate tokens from tokens array. + * + * @param {Array} tokens Array of tokens as defined by `GlFilteredSearch` + * + * @returns {Array} Unique array of tokens + */ +export const uniqueTokens = tokens => { + const knownTokens = []; + return tokens.reduce((uniques, token) => { + if (typeof token === 'object' && token.type !== 'filtered-search-term') { + const tokenString = `${token.type}${token.value.operator}${token.value.data}`; + if (!knownTokens.includes(tokenString)) { + uniques.push(token); + knownTokens.push(tokenString); + } + } else { + uniques.push(token); + } + return uniques; + }, []); }; + +/** + * Creates a token from a type and a filter. Example returned object + * { type: 'myType', value: { data: 'myData', operator: '= '} } + * @param {String} type the name of the filter + * @param {Object} + * @param {Object.value} filter value to be returned as token data + * @param {Object.operator} filter operator to be retuned as token operator + * @return {Object} + * @return {Object.type} token type + * @return {Object.value} token value + */ +function createToken(type, filter) { + return { type, value: { data: filter.value, operator: filter.operator } }; +} + +/** + * This function takes a filter object and translates it into a token array + * @param {Object} filters + * @param {Object.myFilterName} a single filter value or an array of filters + * @return {Array} tokens an array of tokens created from filter values + */ +export function prepareTokens(filters = {}) { + return Object.keys(filters).reduce((memo, key) => { + const value = filters[key]; + if (!value) { + return memo; + } + if (Array.isArray(value)) { + return [...memo, ...value.map(filterValue => createToken(key, filterValue))]; + } + + return [...memo, createToken(key, value)]; + }, []); +} + +export function processFilters(filters) { + return filters.reduce((acc, token) => { + const { type, value } = token; + const { operator } = value; + const tokenValue = value.data; + + if (!acc[type]) { + acc[type] = []; + } + + acc[type].push({ value: tokenValue, operator }); + return acc; + }, {}); +} + +/** + * This function takes a filter object and maps it into a query object. Example filter: + * { myFilterName: { value: 'foo', operator: '=' } } + * gets translated into: + * { myFilterName: 'foo', 'not[myFilterName]': null } + * @param {Object} filters + * @param {Object.myFilterName} a single filter value or an array of filters + * @return {Object} query object with both filter name and not-name with values + */ +export function filterToQueryObject(filters = {}) { + return Object.keys(filters).reduce((memo, key) => { + const filter = filters[key]; + + 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); + } else { + selected = filter?.operator === '=' ? filter.value : null; + unselected = filter?.operator === '!=' ? filter.value : null; + } + + if (isEmpty(selected)) { + selected = null; + } + if (isEmpty(unselected)) { + unselected = null; + } + + return { ...memo, [key]: selected, [`not[${key}]`]: unselected }; + }, {}); +} + +/** + * Extracts filter name from url name, e.g. `not[my_filter]` => `my_filter` + * and returns the operator with it depending on the filter name + * @param {String} filterName from url + * @return {Object} + * @return {Object.filterName} extracted filtern ame + * @return {Object.operator} `=` or `!=` + */ +function extractNameAndOperator(filterName) { + // eslint-disable-next-line @gitlab/require-i18n-strings + if (filterName.startsWith('not[') && filterName.endsWith(']')) { + return { filterName: filterName.slice(4, -1), operator: '!=' }; + } + + return { filterName, operator: '=' }; +} + +/** + * This function takes a URL query string and maps it into a filter object. Example query string: + * '?myFilterName=foo' + * gets translated into: + * { myFilterName: { value: 'foo', operator: '=' } } + * @param {String} query URL quert string, e.g. from `window.location.search` + * @return {Object} filter object with filter names and their values + */ +export function urlQueryToFilter(query = '') { + const filters = queryToObject(query, { gatherArrays: true }); + return Object.keys(filters).reduce((memo, key) => { + const value = filters[key]; + if (!value) { + return memo; + } + const { filterName, operator } = extractNameAndOperator(key); + let previousValues = []; + if (Array.isArray(memo[filterName])) { + previousValues = memo[filterName]; + } + if (Array.isArray(value)) { + const newAdditions = value.filter(Boolean).map(item => ({ value: item, operator })); + return { ...memo, [filterName]: [...previousValues, ...newAdditions] }; + } + + return { ...memo, [filterName]: { value, operator } }; + }, {}); +} 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 969e914ef0c..ee0e00b0f5d 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 @@ -3,7 +3,7 @@ import { GlFilteredSearchToken, GlAvatar, GlFilteredSearchSuggestion, - GlDeprecatedDropdownDivider, + GlDropdownDivider, GlLoadingIcon, } from '@gitlab/ui'; import { debounce } from 'lodash'; @@ -11,15 +11,14 @@ import { debounce } from 'lodash'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { __ } from '~/locale'; -import { ANY_AUTHOR, DEBOUNCE_DELAY } from '../constants'; +import { DEFAULT_LABEL_ANY, DEBOUNCE_DELAY } from '../constants'; export default { - anyAuthor: ANY_AUTHOR, components: { GlFilteredSearchToken, GlAvatar, GlFilteredSearchSuggestion, - GlDeprecatedDropdownDivider, + GlDropdownDivider, GlLoadingIcon, }, props: { @@ -35,6 +34,7 @@ export default { data() { return { authors: this.config.initialAuthors || [], + defaultAuthors: this.config.defaultAuthors || [DEFAULT_LABEL_ANY], loading: true, }; }, @@ -99,10 +99,14 @@ export default { <span>{{ activeAuthor ? activeAuthor.name : inputValue }}</span> </template> <template #suggestions> - <gl-filtered-search-suggestion :value="$options.anyAuthor"> - {{ __('Any') }} + <gl-filtered-search-suggestion + v-for="author in defaultAuthors" + :key="author.value" + :value="author.value" + > + {{ author.text }} </gl-filtered-search-suggestion> - <gl-deprecated-dropdown-divider /> + <gl-dropdown-divider v-if="defaultAuthors.length" /> <gl-loading-icon v-if="loading" /> <template v-else> <gl-filtered-search-suggestion 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 new file mode 100644 index 00000000000..c18bdfc5c20 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue @@ -0,0 +1,115 @@ +<script> +import { + GlToken, + GlFilteredSearchToken, + GlFilteredSearchSuggestion, + GlDropdownDivider, + GlLoadingIcon, +} from '@gitlab/ui'; +import { debounce } from 'lodash'; + +import createFlash from '~/flash'; +import { __ } from '~/locale'; + +import { DEBOUNCE_DELAY } from '../constants'; + +export default { + components: { + GlToken, + GlFilteredSearchToken, + GlFilteredSearchSuggestion, + GlDropdownDivider, + GlLoadingIcon, + }, + props: { + config: { + type: Object, + required: true, + }, + value: { + type: Object, + required: true, + }, + }, + data() { + return { + branches: this.config.initialBranches || [], + defaultBranches: this.config.defaultBranches || [], + loading: true, + }; + }, + computed: { + currentValue() { + return this.value.data.toLowerCase(); + }, + activeBranch() { + return this.branches.find(branch => branch.name.toLowerCase() === this.currentValue); + }, + }, + watch: { + active: { + immediate: true, + handler(newValue) { + if (!newValue && !this.branches.length) { + this.fetchBranchBySearchTerm(this.value.data); + } + }, + }, + }, + methods: { + fetchBranchBySearchTerm(searchTerm) { + this.loading = true; + this.config + .fetchBranches(searchTerm) + .then(({ data }) => { + this.branches = data; + }) + .catch(() => createFlash({ message: __('There was a problem fetching branches.') })) + .finally(() => { + this.loading = false; + }); + }, + searchBranches: debounce(function debouncedSearch({ data }) { + this.fetchBranchBySearchTerm(data); + }, DEBOUNCE_DELAY), + }, +}; +</script> + +<template> + <gl-filtered-search-token + :config="config" + v-bind="{ ...$props, ...$attrs }" + v-on="$listeners" + @input="searchBranches" + > + <template #view-token="{ inputValue }"> + <gl-token variant="search-value">{{ + activeBranch ? activeBranch.name : inputValue + }}</gl-token> + </template> + <template #suggestions> + <gl-filtered-search-suggestion + v-for="branch in defaultBranches" + :key="branch.value" + :value="branch.value" + > + {{ branch.text }} + </gl-filtered-search-suggestion> + <gl-dropdown-divider v-if="defaultBranches.length" /> + <gl-loading-icon v-if="loading" /> + <template v-else> + <gl-filtered-search-suggestion + v-for="branch in branches" + :key="branch.id" + :value="branch.name" + > + <div class="gl-display-flex"> + <span class="gl-display-inline-block gl-mr-3 gl-p-3"></span> + <div>{{ branch.name }}</div> + </div> + </gl-filtered-search-suggestion> + </template> + </template> + </gl-filtered-search-token> +</template> 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 726a1c49993..7a9c5c277eb 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 @@ -3,7 +3,7 @@ import { GlToken, GlFilteredSearchToken, GlFilteredSearchSuggestion, - GlNewDropdownDivider as GlDropdownDivider, + GlDropdownDivider, GlLoadingIcon, } from '@gitlab/ui'; import { debounce } from 'lodash'; @@ -14,10 +14,9 @@ import { __ } from '~/locale'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { stripQuotes } from '../filtered_search_utils'; -import { NO_LABEL, DEBOUNCE_DELAY } from '../constants'; +import { DEFAULT_LABELS, DEBOUNCE_DELAY } from '../constants'; export default { - noLabel: NO_LABEL, components: { GlToken, GlFilteredSearchToken, @@ -38,6 +37,7 @@ export default { data() { return { labels: this.config.initialLabels || [], + defaultLabels: this.config.defaultLabels || DEFAULT_LABELS, loading: true, }; }, @@ -105,10 +105,14 @@ export default { > </template> <template #suggestions> - <gl-filtered-search-suggestion :value="$options.noLabel">{{ - __('No label') - }}</gl-filtered-search-suggestion> - <gl-dropdown-divider /> + <gl-filtered-search-suggestion + v-for="label in defaultLabels" + :key="label.value" + :value="label.value" + > + {{ label.text }} + </gl-filtered-search-suggestion> + <gl-dropdown-divider v-if="defaultLabels.length" /> <gl-loading-icon v-if="loading" /> <template v-else> <gl-filtered-search-suggestion v-for="label in labels" :key="label.id" :value="label.title"> 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 cf1ac4e718b..89952623d0d 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 @@ -2,7 +2,7 @@ import { GlFilteredSearchToken, GlFilteredSearchSuggestion, - GlNewDropdownDivider as GlDropdownDivider, + GlDropdownDivider, GlLoadingIcon, } from '@gitlab/ui'; import { debounce } from 'lodash'; @@ -11,10 +11,9 @@ import createFlash from '~/flash'; import { __ } from '~/locale'; import { stripQuotes } from '../filtered_search_utils'; -import { defaultMilestones, DEBOUNCE_DELAY } from '../constants'; +import { DEFAULT_MILESTONES, DEBOUNCE_DELAY } from '../constants'; export default { - defaultMilestones, components: { GlFilteredSearchToken, GlFilteredSearchSuggestion, @@ -34,6 +33,7 @@ export default { data() { return { milestones: this.config.initialMilestones || [], + defaultMilestones: this.config.defaultMilestones || DEFAULT_MILESTONES, loading: true, }; }, @@ -89,12 +89,13 @@ export default { </template> <template #suggestions> <gl-filtered-search-suggestion - v-for="milestone in $options.defaultMilestones" + v-for="milestone in defaultMilestones" :key="milestone.value" :value="milestone.value" - >{{ milestone.text }}</gl-filtered-search-suggestion > - <gl-dropdown-divider /> + {{ milestone.text }} + </gl-filtered-search-suggestion> + <gl-dropdown-divider v-if="defaultMilestones.length" /> <gl-loading-icon v-if="loading" /> <template v-else> <gl-filtered-search-suggestion diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue deleted file mode 100644 index 58afcebb7b3..00000000000 --- a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue +++ /dev/null @@ -1,154 +0,0 @@ -<script> -import $ from 'jquery'; -import { GlDeprecatedButton } from '@gitlab/ui'; -import { __ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; -/** - * Renders a split dropdown with - * an input that allows to search through the given - * array of options. - * - * When there are no results and `showCreateMode` is true - * it renders a create button with the value typed. - */ -export default { - name: 'FilteredSearchDropdown', - components: { - Icon, - GlDeprecatedButton, - }, - props: { - title: { - type: String, - required: false, - default: '', - }, - buttonType: { - required: false, - validator: value => - ['primary', 'default', 'secondary', 'success', 'info', 'warning', 'danger'].indexOf( - value, - ) !== -1, - default: 'default', - }, - size: { - required: false, - type: String, - default: 'sm', - }, - items: { - type: Array, - required: true, - }, - visibleItems: { - type: Number, - required: false, - default: 5, - }, - filterKey: { - type: String, - required: true, - }, - showCreateMode: { - type: Boolean, - required: false, - default: false, - }, - createButtonText: { - type: String, - required: false, - default: __('Create'), - }, - }, - data() { - return { - filter: '', - }; - }, - computed: { - className() { - return `btn btn-${this.buttonType} btn-${this.size}`; - }, - filteredResults() { - if (this.filter !== '') { - return this.items.filter( - item => - item[this.filterKey] && - item[this.filterKey].toLowerCase().includes(this.filter.toLowerCase()), - ); - } - - return this.items.slice(0, this.visibleItems); - }, - computedCreateButtonText() { - return `${this.createButtonText} ${this.filter}`; - }, - shouldRenderCreateButton() { - return this.showCreateMode && this.filteredResults.length === 0 && this.filter !== ''; - }, - }, - mounted() { - /** - * Resets the filter every time the user closes the dropdown - */ - $(this.$el) - .on('shown.bs.dropdown', () => { - this.$nextTick(() => this.$refs.searchInput.focus()); - }) - .on('hidden.bs.dropdown', () => { - this.filter = ''; - }); - }, -}; -</script> -<template> - <div class="dropdown"> - <div class="btn-group"> - <slot name="mainAction" :class-name="className"> - <button type="button" :class="className">{{ title }}</button> - </slot> - - <button - type="button" - :class="className" - class="dropdown-toggle dropdown-toggle-split" - data-toggle="dropdown" - aria-haspopup="true" - aria-expanded="false" - :aria-label="__('Expand dropdown')" - > - <icon name="angle-down" :size="12" /> - </button> - <div class="dropdown-menu dropdown-menu-right"> - <div class="dropdown-input"> - <input - ref="searchInput" - v-model="filter" - type="search" - :placeholder="__('Filter')" - class="js-filtered-dropdown-input dropdown-input-field" - /> - <icon class="dropdown-input-search" name="search" /> - </div> - - <div class="dropdown-content"> - <ul> - <li v-for="(result, i) in filteredResults" :key="i" class="js-filtered-dropdown-result"> - <slot name="result" :result="result">{{ result[filterKey] }}</slot> - </li> - </ul> - </div> - - <div v-if="shouldRenderCreateButton" class="dropdown-footer"> - <slot name="footer" :filter="filter"> - <gl-deprecated-button - class="js-dropdown-create-button btn-transparent" - @click="$emit('createItem', filter)" - >{{ computedCreateButtonText }}</gl-deprecated-button - > - </slot> - </div> - </div> - </div> - </div> -</template> diff --git a/app/assets/javascripts/vue_shared/components/gl_mentions.vue b/app/assets/javascripts/vue_shared/components/gl_mentions.vue index 00bc46257ed..da4b0aedef5 100644 --- a/app/assets/javascripts/vue_shared/components/gl_mentions.vue +++ b/app/assets/javascripts/vue_shared/components/gl_mentions.vue @@ -9,6 +9,7 @@ const AutoComplete = { Issues: 'issues', Labels: 'labels', Members: 'members', + MergeRequests: 'mergeRequests', }; function doesCurrentLineStartWith(searchString, fullText, selectionStart) { @@ -99,6 +100,14 @@ const autoCompleteMap = { ${icon}`; }, }, + [AutoComplete.MergeRequests]: { + filterValues() { + return this[AutoComplete.MergeRequests]; + }, + menuItemTemplate({ original }) { + return `<small>${original.reference || original.iid}</small> ${escape(original.title)}`; + }, + }, }; export default { @@ -139,6 +148,13 @@ export default { : `~${original.title}`, values: this.getValues(AutoComplete.Labels), }, + { + trigger: '!', + lookup: value => value.iid + value.title, + menuItemTemplate: autoCompleteMap[AutoComplete.MergeRequests].menuItemTemplate, + selectTemplate: ({ original }) => original.reference || `!${original.iid}`, + values: this.getValues(AutoComplete.MergeRequests), + }, ], }); diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index 2625fcc9d09..6ff6f10f786 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import { GlTooltipDirective, GlLink, GlDeprecatedButton } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import CiIconBadge from './ci_badge_link.vue'; diff --git a/app/assets/javascripts/vue_shared/components/help_popover.vue b/app/assets/javascripts/vue_shared/components/help_popover.vue index a57fa09f753..7154360611f 100644 --- a/app/assets/javascripts/vue_shared/components/help_popover.vue +++ b/app/assets/javascripts/vue_shared/components/help_popover.vue @@ -1,6 +1,6 @@ <script> import $ from 'jquery'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlIcon } from '@gitlab/ui'; import { inserted } from '~/feature_highlight/feature_highlight_helper'; import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover'; @@ -11,7 +11,7 @@ import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover export default { name: 'HelpPopover', components: { - Icon, + GlIcon, }, props: { options: { @@ -44,6 +44,6 @@ export default { </script> <template> <button type="button" class="btn btn-blank btn-transparent btn-help" tabindex="0"> - <icon name="question" /> + <gl-icon name="question" /> </button> </template> diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue deleted file mode 100644 index 68eeadf0f25..00000000000 --- a/app/assets/javascripts/vue_shared/components/icon.vue +++ /dev/null @@ -1,72 +0,0 @@ -<script> -import iconsPath from '@gitlab/svgs/dist/icons.svg'; - -// only allow classes in images.scss e.g. s12 -const validSizes = [8, 10, 12, 14, 16, 18, 24, 32, 48, 72]; -let iconValidator = () => true; - -/* - During development/tests we want to validate that we are just using icons that are actually defined -*/ -if (process.env.NODE_ENV !== 'production') { - // eslint-disable-next-line global-require - const data = require('@gitlab/svgs/dist/icons.json'); - const { icons } = data; - iconValidator = value => { - if (icons.includes(value)) { - return true; - } - // eslint-disable-next-line no-console - console.warn(`Icon '${value}' is not a known icon of @gitlab/gitlab-svg`); - return false; - }; -} - -/** This is a re-usable vue component for rendering a svg sprite icon - * @example - * <icon - * name="retry" - * :size="32" - * class="top" - * /> - */ -export default { - props: { - name: { - type: String, - required: true, - validator: iconValidator, - }, - - size: { - type: Number, - required: false, - default: 16, - validator: value => validSizes.includes(value), - }, - }, - - computed: { - spriteHref() { - return `${iconsPath}#${this.name}`; - }, - iconTestClass() { - return `ic-${this.name}`; - }, - iconSizeClass() { - return this.size ? `s${this.size}` : ''; - }, - }, -}; -</script> - -<template> - <svg - :key="spriteHref" - :class="[iconSizeClass, iconTestClass]" - aria-hidden="true" - v-on="$listeners" - > - <use v-bind="{ 'xlink:href': spriteHref }" /> - </svg> -</template> diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue index cfbc5b0df3c..c745ea61f8b 100644 --- a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue +++ b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue @@ -1,13 +1,12 @@ <script> -import { GlTooltip } from '@gitlab/ui'; +import { GlTooltip, GlIcon } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import timeagoMixin from '~/vue_shared/mixins/timeago'; import { timeFor, parsePikadayDate, dateInWords } from '~/lib/utils/datetime_utility'; -import Icon from '~/vue_shared/components/icon.vue'; export default { components: { - Icon, + GlIcon, GlTooltip, }, mixins: [timeagoMixin], @@ -73,7 +72,7 @@ export default { </script> <template> <div ref="milestoneDetails" class="issue-milestone-details"> - <icon :size="16" class="inline icon" name="clock" /> + <gl-icon :size="16" class="gl-mr-2" name="clock" /> <span class="milestone-title d-inline-block">{{ milestone.title }}</span> <gl-tooltip :target="() => $refs.milestoneDetails" placement="bottom" class="js-item-milestone"> <span class="bold">{{ __('Milestone') }}</span> <br /> diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue index 1662e7923b7..2ff4033a07e 100644 --- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue +++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue @@ -1,6 +1,7 @@ <script> +/* eslint-disable vue/no-v-html */ import '~/commons/bootstrap'; -import { GlIcon, GlTooltip, GlTooltipDirective } from '@gitlab/ui'; +import { GlIcon, GlTooltip, GlTooltipDirective, GlButton } from '@gitlab/ui'; import { sprintf } from '~/locale'; import IssueMilestone from './issue_milestone.vue'; import IssueAssignees from './issue_assignees.vue'; @@ -18,6 +19,7 @@ export default { GlTooltip, IssueWeight: () => import('ee_component/boards/components/issue_card_weight.vue'), IssueDueDate, + GlButton, }, directives: { GlTooltip: GlTooltipDirective, @@ -29,6 +31,16 @@ export default { required: false, default: false, }, + isLocked: { + type: Boolean, + required: false, + default: false, + }, + lockedMessage: { + type: String, + required: false, + default: '', + }, }, computed: { stateTitle() { @@ -156,19 +168,27 @@ export default { </div> </div> - <button - v-if="canRemove" + <span + v-if="isLocked" + ref="lockIcon" + v-gl-tooltip + class="gl-px-3 gl-display-inline-block gl-cursor-not-allowed" + :title="lockedMessage" + > + <gl-icon name="lock" /> + </span> + <gl-button + v-else-if="canRemove" ref="removeButton" v-gl-tooltip + icon="close" + category="tertiary" :disabled="removeDisabled" - type="button" - class="btn btn-default btn-svg btn-item-remove js-issue-item-remove-button" + class="js-issue-item-remove-button gl-ml-3" data-qa-selector="remove_related_issue_button" :title="__('Remove')" :aria-label="__('Remove')" @click="onRemoveRequest" - > - <icon :size="16" class="btn-item-remove-icon" name="close" /> - </button> + /> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js b/app/assets/javascripts/vue_shared/components/lib/utils/diff_utils.js index 188ab1769a4..221c4f5b8a8 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 @@ -1,5 +1,3 @@ -/* eslint-disable import/prefer-default-export */ - function trimFirstCharOfLineContent(text) { if (!text) { return text; diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 6df0119c3db..a48c279d0e3 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -1,14 +1,15 @@ <script> +/* eslint-disable vue/no-v-html */ import $ from 'jquery'; import '~/behaviors/markdown/render_gfm'; import { unescape } from 'lodash'; +import { GlIcon } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import { stripHtml } from '~/lib/utils/text_utility'; import { deprecatedCreateFlash as Flash } from '~/flash'; import GLForm from '~/gl_form'; import MarkdownHeader from './header.vue'; import MarkdownToolbar from './toolbar.vue'; -import Icon from '../icon.vue'; import GlMentions from '~/vue_shared/components/gl_mentions.vue'; import Suggestions from '~/vue_shared/components/markdown/suggestions.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -19,7 +20,7 @@ export default { GlMentions, MarkdownHeader, MarkdownToolbar, - Icon, + GlIcon, Suggestions, }, mixins: [glFeatureFlagsMixin()], @@ -168,11 +169,12 @@ export default { emojis: this.enableAutocomplete, members: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, issues: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, - mergeRequests: this.enableAutocomplete, + mergeRequests: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, epics: this.enableAutocomplete, milestones: this.enableAutocomplete, labels: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, snippets: this.enableAutocomplete, + vulnerabilities: this.enableAutocomplete, }); }, beforeDestroy() { @@ -254,7 +256,7 @@ export default { href="#" :aria-label="__('Leave zen mode')" > - <icon :size="16" name="screen-normal" /> + <gl-icon :size="16" name="minimize" /> </a> <markdown-toolbar :markdown-docs-path="markdownDocsPath" diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 7e6edcfbd25..d0a0560846a 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -1,15 +1,15 @@ <script> import $ from 'jquery'; -import { GlPopover, GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { GlPopover, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui'; +import { s__ } from '~/locale'; import { getSelectedFragment } from '~/lib/utils/common_utils'; import { CopyAsGFM } from '../../../behaviors/markdown/copy_as_gfm'; import ToolbarButton from './toolbar_button.vue'; -import Icon from '../icon.vue'; export default { components: { ToolbarButton, - Icon, + GlIcon, GlPopover, GlButton, }, @@ -55,6 +55,15 @@ export default { mdSuggestion() { return ['```suggestion:-0+0', `{text}`, '```'].join('\n'); }, + isMac() { + // Accessing properties using ?. to allow tests to use + // this component without setting up window.gl.client. + // In production, window.gl.client should always be present. + return Boolean(window.gl?.client?.isMac); + }, + modifierKey() { + return this.isMac ? '⌘' : s__('KeyboardKey|Ctrl+'); + }, }, mounted() { $(document).on('markdown-preview:show.vue', this.previewMarkdownTab); @@ -129,8 +138,22 @@ export default { </li> <li :class="{ active: !previewMarkdown }" class="md-header-toolbar"> <div class="d-inline-block"> - <toolbar-button tag="**" :button-title="__('Add bold text')" icon="bold" /> - <toolbar-button tag="*" :button-title="__('Add italic text')" icon="italic" /> + <toolbar-button + tag="**" + :button-title=" + sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey }) + " + shortcuts="mod+b" + icon="bold" + /> + <toolbar-button + tag="_" + :button-title=" + sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey }) + " + shortcuts="mod+i" + icon="italic" + /> <toolbar-button :prepend="true" :tag="tag" @@ -181,7 +204,10 @@ export default { <toolbar-button tag="[{text}](url)" tag-select="url" - :button-title="__('Add a link')" + :button-title=" + sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey }) + " + shortcuts="mod+k" icon="link" /> </div> @@ -221,7 +247,7 @@ export default { :title="__('Go full screen')" type="button" > - <icon name="screen-full" /> + <gl-icon name="maximize" /> </button> </div> </li> 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 4de80e9b4c2..1fc54d2f52e 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue @@ -1,11 +1,10 @@ <script> -import { GlDeprecatedButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +import { GlButton, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { - components: { Icon, GlDeprecatedButton, GlLoadingIcon }, + components: { GlIcon, GlButton, GlLoadingIcon }, directives: { 'gl-tooltip': GlTooltipDirective }, mixins: [glFeatureFlagsMixin()], props: { @@ -97,7 +96,7 @@ export default { <div class="qa-suggestion-diff-header js-suggestion-diff-header font-weight-bold"> {{ __('Suggested change') }} <a v-if="helpPagePath" :href="helpPagePath" :aria-label="__('Help')" class="js-help-btn"> - <icon name="question-o" css-classes="link-highlight" /> + <gl-icon name="question-o" css-classes="link-highlight" /> </a> </div> <div v-if="isApplied" class="badge badge-success">{{ __('Applied') }}</div> @@ -106,14 +105,14 @@ export default { <span>{{ applyingSuggestionsMessage }}</span> </div> <div v-else-if="canApply && canBeBatched && isBatched" class="d-flex align-items-center"> - <gl-deprecated-button + <gl-button class="btn-inverted js-remove-from-batch-btn btn-grouped" :disabled="isApplying" @click="removeSuggestionFromBatch" > {{ __('Remove from batch') }} - </gl-deprecated-button> - <gl-deprecated-button + </gl-button> + <gl-button v-gl-tooltip.viewport="__('This also resolves all related threads')" class="btn-inverted js-apply-batch-btn btn-grouped" :disabled="isApplying" @@ -124,26 +123,26 @@ export default { <span class="badge badge-pill badge-pill-success"> {{ batchSuggestionsCount }} </span> - </gl-deprecated-button> + </gl-button> </div> <div v-else class="d-flex align-items-center"> - <gl-deprecated-button + <gl-button v-if="canBeBatched && !isDisableButton" class="btn-inverted js-add-to-batch-btn btn-grouped" :disabled="isDisableButton" @click="addSuggestionToBatch" > {{ __('Add suggestion to batch') }} - </gl-deprecated-button> + </gl-button> <span v-gl-tooltip.viewport="tooltipMessage" tabindex="0"> - <gl-deprecated-button + <gl-button class="btn-inverted js-apply-btn btn-grouped" :disabled="isDisableButton" variant="success" @click="applySuggestion" > {{ __('Apply suggestion') }} - </gl-deprecated-button> + </gl-button> </span> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue index 112bd03b49b..9059f0d2a8b 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ export default { name: 'SuggestionDiffRow', props: { @@ -26,9 +27,14 @@ export default { <td class="diff-line-num new_line border-top-0 border-bottom-0" :class="lineType"> {{ line.new_line }} </td> - <td class="line_content" :class="[{ 'd-table-cell': displayAsCell }, lineType]"> - <span v-if="line.rich_text" v-html="line.rich_text"></span> - <span v-else-if="line.text">{{ line.text }}</span> + <td + class="line_content" + :class="[{ 'd-table-cell': displayAsCell }, lineType]" + data-testid="suggestion-diff-content" + > + <span v-if="line.rich_text" class="line" v-html="line.rich_text"></span> + <span v-else-if="line.text" class="line">{{ line.text }}</span> + <span v-else class="line"></span> </td> </tr> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue index 1216484b35f..083f581af05 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue @@ -1,10 +1,14 @@ <script> import Vue from 'vue'; +import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import { __ } from '~/locale'; import SuggestionDiff from './suggestion_diff.vue'; import { deprecatedCreateFlash as Flash } from '~/flash'; export default { + directives: { + SafeHtml, + }, props: { lineType: { type: String, @@ -115,6 +119,6 @@ export default { <template> <div> <div class="flash-container js-suggestions-flash"></div> - <div v-show="isRendered" ref="container" class="md" v-html="noteHtml"></div> + <div v-show="isRendered" ref="container" v-safe-html="noteHtml" class="md"></div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue index f37dd9e171c..6c35741e7e5 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue @@ -1,10 +1,9 @@ <script> -import { GlTooltipDirective } from '@gitlab/ui'; -import Icon from '../icon.vue'; +import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; export default { components: { - Icon, + GlIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -47,6 +46,26 @@ export default { required: false, default: 0, }, + + /** + * A string (or an array of strings) of + * [mousetrap](https://craig.is/killing/mice) keyboard shortcuts + * that should be attached to this button. For example: + * "command+k" + * ...or... + * ["command+k", "ctrl+k"] + */ + shortcuts: { + type: [String, Array], + required: false, + default: () => [], + }, + }, + computed: { + shortcutsString() { + const shortcutArray = Array.isArray(this.shortcuts) ? this.shortcuts : [this.shortcuts]; + return JSON.stringify(shortcutArray); + }, }, }; </script> @@ -60,6 +79,7 @@ export default { :data-md-block="tagBlock" :data-md-tag-content="tagContent" :data-md-prepend="prepend" + :data-md-shortcuts="shortcutsString" :title="buttonTitle" :aria-label="buttonTitle" type="button" @@ -67,6 +87,6 @@ export default { data-container="body" @click="() => $emit('click')" > - <icon :name="icon" /> + <gl-icon :name="icon" /> </button> </template> 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 69ba5cb97e2..35ba7c665d5 100644 --- a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue +++ b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue @@ -1,14 +1,13 @@ <script> import $ from 'jquery'; -import { GlDeprecatedButton, GlTooltipDirective } from '@gitlab/ui'; +import { GlDeprecatedButton, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import Clipboard from 'clipboard'; import { __ } from '~/locale'; -import Icon from '~/vue_shared/components/icon.vue'; export default { components: { GlDeprecatedButton, - Icon, + GlIcon, }, directives: { @@ -121,7 +120,7 @@ export default { :title="title" > <slot> - <icon name="copy-to-clipboard" /> + <gl-icon name="copy-to-clipboard" /> </slot> </gl-deprecated-button> </template> diff --git a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue index f986b105f20..c12012d8419 100644 --- a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue +++ b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue @@ -1,8 +1,8 @@ <script> -import { GlLink } from '@gitlab/ui'; +/* eslint-disable vue/no-v-html */ +import { GlLink, GlIcon } from '@gitlab/ui'; import { escape } from 'lodash'; import { __, sprintf } from '~/locale'; -import icon from '../icon.vue'; function buildDocsLinkStart(path) { return `<a href="${escape(path)}" target="_blank" rel="noopener noreferrer">`; @@ -16,7 +16,7 @@ const NoteableTypeText = { export default { components: { - icon, + GlIcon, GlLink, }, props: { @@ -89,7 +89,7 @@ export default { </script> <template> <div class="issuable-note-warning"> - <icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" /> + <gl-icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" /> <span v-if="isLockedAndConfidential" ref="lockedAndConfidential"> <span v-html="confidentialAndLockedDiscussionText"></span> diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue index e75ac8c54bc..53dbae39608 100644 --- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue @@ -1,5 +1,5 @@ <script> -import { GlSkeletonLoading } from '@gitlab/ui'; +import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; export default { 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 fe57d4f29ca..f30676e8ef3 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -1,4 +1,6 @@ <script> +/* eslint-disable vue/no-v-html */ + /** * Common component to render a system note, icon and user information. * @@ -18,10 +20,15 @@ */ import $ from 'jquery'; import { mapGetters, mapActions, mapState } from 'vuex'; -import { GlDeprecatedButton, GlSkeletonLoading, GlTooltipDirective } from '@gitlab/ui'; +import { + GlButton, + GlDeprecatedSkeletonLoading as GlSkeletonLoading, + GlTooltipDirective, + GlIcon, + GlSafeHtmlDirective as SafeHtml, +} from '@gitlab/ui'; import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history'; import noteHeader from '~/notes/components/note_header.vue'; -import Icon from '~/vue_shared/components/icon.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import TimelineEntryItem from './timeline_entry_item.vue'; import { spriteIcon } from '../../../lib/utils/common_utils'; @@ -32,14 +39,15 @@ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3; export default { name: 'SystemNote', components: { - Icon, + GlIcon, noteHeader, TimelineEntryItem, - GlDeprecatedButton, + GlButton, GlSkeletonLoading, }, directives: { GlTooltip: GlTooltipDirective, + SafeHtml, }, mixins: [descriptionVersionHistoryMixin, glFeatureFlagsMixin()], props: { @@ -104,25 +112,28 @@ export default { <div class="timeline-content"> <div class="note-header"> <note-header :author="note.author" :created-at="note.created_at" :note-id="note.id"> - <span v-html="actionTextHtml"></span> + <span v-safe-html="actionTextHtml"></span> <template v-if="canSeeDescriptionVersion" slot="extra-controls"> · - <button type="button" class="btn-blank btn-link" @click="toggleDescriptionVersion"> - {{ __('Compare with previous version') }} - <icon :name="descriptionVersionToggleIcon" :size="12" class="append-left-5" /> - </button> + <gl-button + variant="link" + :icon="descriptionVersionToggleIcon" + data-testid="compare-btn" + @click="toggleDescriptionVersion" + >{{ __('Compare with previous version') }}</gl-button + > </template> </note-header> </div> <div class="note-body"> <div + v-safe-html="note.note_html" :class="{ 'system-note-commit-list': hasMoreCommits, 'hide-shade': expanded }" class="note-text md" - v-html="note.note_html" ></div> <div v-if="hasMoreCommits" class="flex-list"> <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded"> - <icon :name="toggleIcon" :size="8" class="gl-mr-2" /> + <gl-icon :name="toggleIcon" :size="8" class="gl-mr-2" /> <span>{{ __('Toggle commit list') }}</span> </div> </div> @@ -130,17 +141,18 @@ export default { <pre v-if="isLoadingDescriptionVersion" class="loading-state"> <gl-skeleton-loading /> </pre> - <pre v-else class="wrapper mt-2" v-html="descriptionVersion"></pre> - <gl-deprecated-button + <pre v-else v-safe-html="descriptionVersion" class="wrapper mt-2"></pre> + <gl-button v-if="displayDeleteButton" - ref="deleteDescriptionVersionButton" v-gl-tooltip :title="__('Remove description history')" - class="btn-transparent delete-description-history" + variant="default" + category="tertiary" + icon="remove" + class="delete-description-history" + data-testid="delete-description-version-button" @click="deleteDescriptionVersion" - > - <icon name="remove" /> - </gl-deprecated-button> + /> </div> </div> </div> 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 e053a9ddaa6..154671fe9fa 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 @@ -1,14 +1,14 @@ <script> -import { GlDeprecatedButton } from '@gitlab/ui'; +/* eslint-disable vue/no-v-html */ +import { GlButton, GlIcon } from '@gitlab/ui'; import { isString } from 'lodash'; -import Icon from '~/vue_shared/components/icon.vue'; import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue'; import highlight from '~/lib/utils/highlight'; import { truncateNamespace } from '~/lib/utils/text_utility'; export default { name: 'ProjectListItem', - components: { Icon, ProjectAvatar, GlDeprecatedButton }, + components: { GlIcon, ProjectAvatar, GlButton }, props: { project: { type: Object, @@ -40,17 +40,16 @@ export default { }; </script> <template> - <gl-deprecated-button - class="d-flex align-items-center btn pt-1 pb-1 border-0 project-list-item" + <gl-button + category="tertiary" + class="gl-display-flex gl-align-items-center gl-justify-content-start! gl-mb-2 gl-w-full" @click="onClick" > - <icon - class="gl-ml-3 gl-mr-3 flex-shrink-0 position-top-0 js-selected-icon" - :class="{ 'js-selected visible': selected, 'js-unselected invisible': !selected }" - name="mobile-issue-close" - /> - <project-avatar class="flex-shrink-0 js-project-avatar" :project="project" :size="32" /> - <div class="d-flex flex-wrap project-namespace-name-container"> + <div + class="gl-display-flex gl-align-items-center gl-flex-wrap project-namespace-name-container" + > + <gl-icon v-if="selected" class="js-selected-icon" name="mobile-issue-close" /> + <project-avatar class="gl-flex-shrink-0 js-project-avatar" :project="project" :size="32" /> <div v-if="truncatedNamespace" :title="projectNameWithNamespace" @@ -65,5 +64,5 @@ export default { v-html="highlightedProjectName" ></div> </div> - </gl-deprecated-button> + </gl-button> </template> 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 0b91588a006..4e2029cd74f 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 @@ -100,7 +100,7 @@ export default { @bottomReached="bottomReached" > <template v-if="!showLoadingIndicator" #items> - <div class="d-flex flex-column"> + <div class="gl-display-flex gl-flex-direction-column gl-p-3"> <project-list-item v-for="project in projectSearchResults" :key="project.id" diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue index 25701df33f3..fc1f3675a3d 100644 --- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue +++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue @@ -1,4 +1,5 @@ <script> +/* eslint-disable vue/no-v-html */ import DeprecatedModal from './deprecated_modal.vue'; import { eventHub } from './recaptcha_eventhub'; diff --git a/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue b/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue new file mode 100644 index 00000000000..08ee23d25bf --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/registry/code_instruction.vue @@ -0,0 +1,82 @@ +<script> +import { uniqueId } from 'lodash'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import Tracking from '~/tracking'; + +export default { + name: 'CodeInstruction', + components: { + ClipboardButton, + }, + mixins: [Tracking.mixin()], + props: { + label: { + type: String, + required: false, + default: '', + }, + instruction: { + type: String, + required: true, + }, + copyText: { + type: String, + required: true, + }, + multiline: { + type: Boolean, + required: false, + default: false, + }, + trackingAction: { + type: String, + required: false, + default: '', + }, + trackingLabel: { + type: String, + required: false, + default: '', + }, + }, + created() { + this.uniqueId = uniqueId(); + }, + methods: { + trackCopy() { + if (this.trackingAction) { + this.track(this.trackingAction, { label: this.trackingLabel }); + } + }, + generateFormId(name) { + return `${name}_${this.uniqueId}`; + }, + }, +}; +</script> + +<template> + <div v-if="!multiline" class="gl-mb-3"> + <label v-if="label" :for="generateFormId('instruction-input')">{{ label }}</label> + <div class="input-group gl-mb-3"> + <input + :id="generateFormId('instruction-input')" + :value="instruction" + type="text" + class="form-control gl-font-monospace" + data-testid="instruction-input" + readonly + @copy="trackCopy" + /> + <span class="input-group-append" data-testid="instruction-button" @click="trackCopy"> + <clipboard-button :text="instruction" :title="copyText" class="input-group-text" /> + </span> + </div> + </div> + + <div v-else> + <pre class="gl-font-monospace" data-testid="multiline-instruction" @copy="trackCopy">{{ + instruction + }}</pre> + </div> +</template> diff --git a/app/assets/javascripts/registry/shared/components/details_row.vue b/app/assets/javascripts/vue_shared/components/registry/details_row.vue index 2e245fadead..2e245fadead 100644 --- a/app/assets/javascripts/registry/shared/components/details_row.vue +++ b/app/assets/javascripts/vue_shared/components/registry/details_row.vue diff --git a/app/assets/javascripts/packages/details/components/history_element.vue b/app/assets/javascripts/vue_shared/components/registry/history_item.vue index 8a51c1528cf..a60b630b207 100644 --- a/app/assets/javascripts/packages/details/components/history_element.vue +++ b/app/assets/javascripts/vue_shared/components/registry/history_item.vue @@ -3,12 +3,11 @@ import { GlIcon } from '@gitlab/ui'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; export default { - name: 'HistoryElement', + name: 'HistoryItem', components: { GlIcon, TimelineEntryItem, }, - props: { icon: { type: String, @@ -29,7 +28,9 @@ export default { <slot></slot> </span> </div> - <div class="note-body"></div> + <div class="note-body"> + <slot name="body"></slot> + </div> </div> </timeline-entry-item> </template> diff --git a/app/assets/javascripts/registry/explorer/components/list_item.vue b/app/assets/javascripts/vue_shared/components/registry/list_item.vue index c57645cc3a1..50a19dc2156 100644 --- a/app/assets/javascripts/registry/explorer/components/list_item.vue +++ b/app/assets/javascripts/vue_shared/components/registry/list_item.vue @@ -10,11 +10,6 @@ export default { default: false, required: false, }, - last: { - type: Boolean, - default: false, - required: false, - }, disabled: { type: Boolean, default: false, @@ -35,12 +30,10 @@ export default { computed: { optionalClasses() { return { - 'gl-border-t-1': !this.first, - 'gl-border-t-2': this.first, - 'gl-border-b-1': !this.last, - 'gl-border-b-2': this.last, + 'gl-border-t-transparent': !this.first && !this.selected, + 'gl-border-t-gray-100': this.first && !this.selected, 'disabled-content': this.disabled, - 'gl-border-gray-100': !this.selected, + 'gl-border-b-gray-100': !this.selected, 'gl-bg-blue-50 gl-border-blue-200': this.selected, }; }, @@ -58,21 +51,26 @@ export default { <template> <div - class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid" + class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1" :class="optionalClasses" > - <div class="gl-display-flex gl-align-items-center gl-py-4 gl-px-2"> + <div class="gl-display-flex gl-align-items-center gl-py-5"> <div v-if="$slots['left-action']" class="gl-w-7 gl-display-none gl-display-sm-flex gl-justify-content-start gl-pl-2" > <slot name="left-action"></slot> </div> - <div class="gl-display-flex gl-flex-direction-column gl-flex-fill-1"> + <div + class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-fill-1" + > <div - class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-text-body gl-font-weight-bold" + class="gl-display-flex gl-flex-direction-column gl-justify-content-space-between gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1" > - <div class="gl-display-flex gl-align-items-center"> + <div + v-if="$slots['left-primary']" + class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0" + > <slot name="left-primary"></slot> <gl-button v-if="detailsSlots.length > 0" @@ -83,24 +81,33 @@ export default { @click="toggleDetails" /> </div> - <div> - <slot name="right-primary"></slot> + <div + v-if="$slots['left-secondary']" + class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1 gl-min-h-6 gl-min-w-0 gl-flex-fill-1" + > + <slot name="left-secondary"></slot> </div> </div> <div - class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-font-sm gl-text-gray-300" + class="gl-display-flex gl-flex-direction-column gl-sm-align-items-flex-end gl-justify-content-space-between gl-text-gray-500 gl-flex-shrink-0" > - <div> - <slot name="left-secondary"></slot> + <div + v-if="$slots['right-primary']" + class="gl-display-flex gl-align-items-center gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6" + > + <slot name="right-primary"></slot> </div> - <div> + <div + v-if="$slots['right-secondary']" + class="gl-display-flex gl-align-items-center gl-mt-1 gl-min-h-6" + > <slot name="right-secondary"></slot> </div> </div> </div> <div v-if="$slots['right-action']" - class="gl-w-9 gl-display-none gl-display-sm-flex gl-justify-content-end gl-pr-2" + class="gl-w-9 gl-display-none gl-display-sm-flex gl-justify-content-end gl-pr-1" > <slot name="right-action"></slot> </div> diff --git a/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue b/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue new file mode 100644 index 00000000000..8ef623b68eb --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/registry/metadata_item.vue @@ -0,0 +1,63 @@ +<script> +import { GlIcon, GlLink } from '@gitlab/ui'; +import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; + +export default { + name: 'MetadataItem', + components: { + GlIcon, + GlLink, + TooltipOnTruncate, + }, + props: { + icon: { + type: String, + required: false, + default: null, + }, + text: { + type: String, + required: true, + }, + link: { + type: String, + required: false, + default: '', + }, + size: { + type: String, + required: false, + default: 's', + validator(value) { + return !value || ['xs', 's', 'm', 'l', 'xl'].includes(value); + }, + }, + }, + computed: { + sizeClass() { + return `mw-${this.size}`; + }, + }, +}; +</script> + +<template> + <div class="gl-display-inline-flex gl-align-items-center"> + <gl-icon v-if="icon" :name="icon" class="gl-text-gray-500 gl-mr-3" /> + <tooltip-on-truncate v-if="link" :title="text" class="gl-text-truncate" :class="sizeClass"> + <gl-link :href="link" class="gl-font-weight-bold"> + {{ text }} + </gl-link> + </tooltip-on-truncate> + <div + v-else + data-testid="metadata-item-text" + class="gl-font-weight-bold gl-display-inline-flex" + :class="sizeClass" + > + <tooltip-on-truncate :title="text" class="gl-text-truncate"> + {{ text }} + </tooltip-on-truncate> + </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 new file mode 100644 index 00000000000..cc33b8f85cd --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/registry/title_area.vue @@ -0,0 +1,66 @@ +<script> +import { GlAvatar } from '@gitlab/ui'; + +export default { + name: 'TitleArea', + components: { + GlAvatar, + }, + props: { + avatar: { + type: String, + default: null, + required: false, + }, + title: { + type: String, + default: null, + required: false, + }, + }, + data() { + return { + metadataSlots: [], + }; + }, + mounted() { + this.metadataSlots = Object.keys(this.$slots).filter(k => k.startsWith('metadata_')); + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-justify-content-space-between gl-py-3"> + <div class="gl-flex-direction-column"> + <div class="gl-display-flex"> + <gl-avatar v-if="avatar" :src="avatar" shape="rect" class="gl-align-self-center gl-mr-4" /> + + <div class="gl-display-flex gl-flex-direction-column"> + <h1 class="gl-font-size-h1 gl-mt-3 gl-mb-2" data-testid="title"> + <slot name="title">{{ title }}</slot> + </h1> + + <div + v-if="$slots['sub-header']" + class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1" + > + <slot name="sub-header"></slot> + </div> + </div> + </div> + + <div class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3"> + <div + v-for="(row, metadataIndex) in metadataSlots" + :key="metadataIndex" + class="gl-display-flex gl-align-items-center gl-mr-5" + > + <slot :name="row"></slot> + </div> + </div> + </div> + <div v-if="$slots['right-actions']" class="gl-mt-3"> + <slot name="right-actions"></slot> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/resizable_chart/constants.js b/app/assets/javascripts/vue_shared/components/resizable_chart/constants.js index edc5ffb7b77..68d86777995 100644 --- a/app/assets/javascripts/vue_shared/components/resizable_chart/constants.js +++ b/app/assets/javascripts/vue_shared/components/resizable_chart/constants.js @@ -1,6 +1,6 @@ export const DEFAULT_RX = 0.4; -export const DEFAULT_BAR_WIDTH = 6; -export const DEFAULT_LABEL_WIDTH = 4; -export const DEFAULT_LABEL_HEIGHT = 5; +export const DEFAULT_BAR_WIDTH = 4; +export const DEFAULT_LABEL_WIDTH = 3; +export const DEFAULT_LABEL_HEIGHT = 3; export const BAR_HEIGHTS = [5, 7, 9, 14, 21, 35, 50, 80]; export const GRID_YS = [30, 60, 90]; diff --git a/app/assets/javascripts/vue_shared/components/resizable_chart/skeleton_loader.vue b/app/assets/javascripts/vue_shared/components/resizable_chart/skeleton_loader.vue index 306fa61780f..a9f35a73db0 100644 --- a/app/assets/javascripts/vue_shared/components/resizable_chart/skeleton_loader.vue +++ b/app/assets/javascripts/vue_shared/components/resizable_chart/skeleton_loader.vue @@ -61,35 +61,37 @@ export default { }; </script> <template> - <gl-skeleton-loader :unique-key="uniqueKey"> - <rect - v-for="(y, index) in $options.GRID_YS" - :key="`grid-${index}`" - data-testid="skeleton-chart-grid" - x="0" - :y="`${y}%`" - width="100%" - height="1px" - /> - <rect - v-for="(height, index) in $options.BAR_HEIGHTS" - :key="`bar-${index}`" - data-testid="skeleton-chart-bar" - :x="`${getBarXPosition(index)}%`" - :y="`${90 - height}%`" - :width="`${barWidth}%`" - :height="`${height}%`" - :rx="`${rx}%`" - /> - <rect - v-for="(height, index) in $options.BAR_HEIGHTS" - :key="`label-${index}`" - data-testid="skeleton-chart-label" - :x="`${labelCentering + getBarXPosition(index)}%`" - :y="`${100 - labelHeight}%`" - :width="`${labelWidth}%`" - :height="`${labelHeight}%`" - :rx="`${rx}%`" - /> - </gl-skeleton-loader> + <div class="gl-px-8"> + <gl-skeleton-loader :unique-key="uniqueKey" class="gl-p-8"> + <rect + v-for="(y, index) in $options.GRID_YS" + :key="`grid-${index}`" + data-testid="skeleton-chart-grid" + x="0" + :y="`${y}%`" + width="100%" + height="1px" + /> + <rect + v-for="(height, index) in $options.BAR_HEIGHTS" + :key="`bar-${index}`" + data-testid="skeleton-chart-bar" + :x="`${getBarXPosition(index)}%`" + :y="`${90 - height}%`" + :width="`${barWidth}%`" + :height="`${height}%`" + :rx="`${rx}%`" + /> + <rect + v-for="(height, index) in $options.BAR_HEIGHTS" + :key="`label-${index}`" + data-testid="skeleton-chart-label" + :x="`${labelCentering + getBarXPosition(index)}%`" + :y="`${100 - labelHeight}%`" + :width="`${labelWidth}%`" + :height="`${labelHeight}%`" + :rx="`${rx}%`" + /> + </gl-skeleton-loader> + </div> </template> 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 a9c5d442f62..108c60c3edb 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 @@ -1,17 +1,19 @@ import { union, mapValues } from 'lodash'; import renderBlockHtml from './renderers/render_html_block'; -import renderKramdownList from './renderers/render_kramdown_list'; -import renderKramdownText from './renderers/render_kramdown_text'; +import renderHeading from './renderers/render_heading'; import renderIdentifierInstanceText from './renderers/render_identifier_instance_text'; import renderIdentifierParagraph from './renderers/render_identifier_paragraph'; import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline'; import renderSoftbreak from './renderers/render_softbreak'; +import renderAttributeDefinition from './renderers/render_attribute_definition'; +import renderListItem from './renderers/render_list_item'; const htmlInlineRenderers = [renderFontAwesomeHtmlInline]; const htmlBlockRenderers = [renderBlockHtml]; -const listRenderers = [renderKramdownList]; -const paragraphRenderers = [renderIdentifierParagraph]; -const textRenderers = [renderKramdownText, renderIdentifierInstanceText]; +const headingRenderers = [renderHeading]; +const paragraphRenderers = [renderIdentifierParagraph, renderBlockHtml]; +const textRenderers = [renderIdentifierInstanceText, renderAttributeDefinition]; +const listItemRenderers = [renderListItem]; const softbreakRenderers = [renderSoftbreak]; const executeRenderer = (renderers, node, context) => { @@ -25,7 +27,8 @@ const buildCustomHTMLRenderer = customRenderers => { ...customRenderers, htmlBlock: union(htmlBlockRenderers, customRenderers?.htmlBlock), htmlInline: union(htmlInlineRenderers, customRenderers?.htmlInline), - list: union(listRenderers, customRenderers?.list), + heading: union(headingRenderers, customRenderers?.heading), + item: union(listItemRenderers, customRenderers?.listItem), paragraph: union(paragraphRenderers, customRenderers?.paragraph), text: union(textRenderers, customRenderers?.text), softbreak: union(softbreakRenderers, customRenderers?.softbreak), 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 868ede9426e..2bce691e793 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 @@ -28,6 +28,8 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) => const orderedListItemNode = 'OL LI'; const emphasisNode = 'EM, I'; const strongNode = 'STRONG, B'; + const headingNode = 'H1, H2, H3, H4, H5, H6'; + const preCodeNode = 'PRE CODE'; return { TEXT_NODE(node) { @@ -63,8 +65,10 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) => }, [unorderedListItemNode](node, subContent) { const baseResult = baseRenderer.convert(node, subContent); + const formatted = baseResult.replace(/^(\s*)([*|-])/, `$1${unorderedListBulletChar}`); + const { attributeDefinition } = node.dataset; - return baseResult.replace(/^(\s*)([*|-])/, `$1${unorderedListBulletChar}`); + return attributeDefinition ? `${formatted.trimRight()}\n${attributeDefinition}\n` : formatted; }, [orderedListItemNode](node, subContent) { const baseResult = baseRenderer.convert(node, subContent); @@ -82,6 +86,19 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) => return result.replace(/^[*_]{2}/, strongSyntax).replace(/[*_]{2}$/, strongSyntax); }, + [headingNode](node, subContent) { + const result = baseRenderer.convert(node, subContent); + const { attributeDefinition } = node.dataset; + + return attributeDefinition ? `${result.trimRight()}\n${attributeDefinition}\n\n` : result; + }, + [preCodeNode](node, subContent) { + const isReferenceDefinition = Boolean(node.dataset.sseReferenceDefinition); + + return isReferenceDefinition + ? `\n\n${node.innerText}\n\n` + : baseRenderer.convert(node, subContent); + }, }; }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js new file mode 100644 index 00000000000..bd419447a48 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_attribute_definition.js @@ -0,0 +1,7 @@ +import { isAttributeDefinition } from './render_utils'; + +const canRender = ({ literal }) => isAttributeDefinition(literal); + +const render = () => ({ type: 'html', content: '<!-- sse-attribute-definition -->' }); + +export default { canRender, render }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js new file mode 100644 index 00000000000..71026fd0d65 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_heading.js @@ -0,0 +1,6 @@ +import { + renderWithAttributeDefinitions as render, + willAlwaysRender as canRender, +} from './render_utils'; + +export default { render, canRender }; 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 4ec45ecd3a7..3f9c6291d1b 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,5 +1,3 @@ -import { renderUneditableBranch as render } from './render_utils'; - const identifierRegex = /(^\[.+\]: .+)/; const isIdentifier = text => { @@ -10,4 +8,33 @@ const canRender = (node, context) => { return isIdentifier(context.getChildrenText(node)); }; +const getReferenceDefinitions = (node, definitions = '') => { + if (!node) { + return definitions; + } + + const definition = node.type === 'text' ? node.literal : '\n'; + + return getReferenceDefinitions(node.next, `${definitions}${definition}`); +}; + +const render = (node, { skipChildren }) => { + const content = getReferenceDefinitions(node.firstChild); + + skipChildren(); + + return [ + { + type: 'openTag', + tagName: 'pre', + classNames: ['code-block', 'language-markdown'], + attributes: { 'data-sse-reference-definition': true }, + }, + { type: 'openTag', tagName: 'code' }, + { type: 'text', content }, + { type: 'closeTag', tagName: 'code' }, + { type: 'closeTag', tagName: 'pre' }, + ]; +}; + export default { canRender, render }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js deleted file mode 100644 index 949ca0e5c2a..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js +++ /dev/null @@ -1,24 +0,0 @@ -import { renderUneditableBranch as render } from './render_utils'; - -const isKramdownTOC = ({ type, literal }) => type === 'text' && literal === 'TOC'; - -const canRender = node => { - let targetNode = node; - while (targetNode !== null) { - const { firstChild } = targetNode; - const isLeaf = firstChild === null; - if (isLeaf) { - if (isKramdownTOC(targetNode)) { - return true; - } - - break; - } - - targetNode = targetNode.firstChild; - } - - return false; -}; - -export default { canRender, render }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js deleted file mode 100644 index 0551894918c..00000000000 --- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js +++ /dev/null @@ -1,9 +0,0 @@ -import { renderUneditableLeaf as render } from './render_utils'; - -const kramdownRegex = /(^{:.+}$)/; - -const canRender = ({ literal }) => { - return kramdownRegex.test(literal); -}; - -export default { canRender, render }; diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js new file mode 100644 index 00000000000..71026fd0d65 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_list_item.js @@ -0,0 +1,6 @@ +import { + renderWithAttributeDefinitions as render, + willAlwaysRender as canRender, +} from './render_utils'; + +export default { render, canRender }; 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 cec6491557b..4cba2c70486 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 @@ -8,3 +8,31 @@ export const renderUneditableLeaf = (_, { origin }) => buildUneditableBlockToken export const renderUneditableBranch = (_, { entering, origin }) => entering ? buildUneditableOpenTokens(origin()) : buildUneditableCloseToken(); + +const attributeDefinitionRegexp = /(^{:.+}$)/; + +export const isAttributeDefinition = text => attributeDefinitionRegexp.test(text); + +const findAttributeDefinition = node => { + const literal = + node?.next?.firstChild?.literal || node?.firstChild?.firstChild?.next?.next?.literal; // for headings // for list items; + + return isAttributeDefinition(literal) ? literal : null; +}; + +export const renderWithAttributeDefinitions = (node, { origin }) => { + const attributes = findAttributeDefinition(node); + const token = origin(); + + if (token.type === 'openTag' && attributes) { + Object.assign(token, { + attributes: { + 'data-attribute-definition': attributes, + }, + }); + } + + return token; +}; + +export const willAlwaysRender = () => true; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue index cc24fedceed..0ed5a050fe4 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue @@ -1,4 +1,5 @@ <script> +import { GlIcon } from '@gitlab/ui'; import tooltip from '~/vue_shared/directives/tooltip'; export default { @@ -6,6 +7,9 @@ export default { directives: { tooltip, }, + components: { + GlIcon, + }, props: { containerClass: { type: String, @@ -47,7 +51,7 @@ export default { data-boundary="viewport" @click="click" > - <i v-if="showIcon" class="fa fa-calendar" aria-hidden="true"> </i> + <gl-icon v-if="showIcon" name="calendar" /> <slot> <span> {{ text }} </span> </slot> 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 5eef439aa90..1ef3d5627ae 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 @@ -14,7 +14,10 @@ import DropdownSearchInput from './dropdown_search_input.vue'; import DropdownFooter from './dropdown_footer.vue'; import DropdownCreateLabel from './dropdown_create_label.vue'; +import { DropdownVariant } from '../labels_select_vue/constants'; + export default { + DropdownVariant, components: { DropdownTitle, DropdownValue, @@ -80,6 +83,11 @@ export default { required: false, default: false, }, + variant: { + type: String, + required: false, + default: DropdownVariant.Sidebar, + }, }, computed: { hiddenInputName() { @@ -123,7 +131,7 @@ export default { <template> <div class="block labels js-labels-block"> <dropdown-value-collapsed - v-if="showCreate" + v-if="showCreate && variant === $options.DropdownVariant.Sidebar" :labels="context.labels" @onValueClick="handleCollapsedValueClick" /> @@ -150,18 +158,21 @@ export default { :labels-path="labelsPath" :namespace="namespace" :labels="context.labels" - :show-extra-options="!showCreate" + :show-extra-options="!showCreate || variant !== $options.DropdownVariant.Sidebar" :enable-scoped-labels="enableScopedLabels" /> <div - class="dropdown-menu dropdown-select dropdown-menu-paging -dropdown-menu-labels dropdown-menu-selectable" + class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable" > <div class="dropdown-page-one"> - <dropdown-header v-if="showCreate" /> + <dropdown-header v-if="showCreate && variant === $options.DropdownVariant.Sidebar" /> <dropdown-search-input /> <div class="dropdown-content" data-qa-selector="labels_dropdown_content"></div> - <div class="dropdown-loading"><gl-loading-icon /></div> + <div class="dropdown-loading"> + <gl-loading-icon + class="gl-display-flex gl-justify-content-center gl-align-items-center gl-h-full" + /> + </div> <dropdown-footer v-if="showCreate" :labels-web-url="labelsWebUrl" 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 74c5e063c3d..434aabc3df9 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 @@ -1,7 +1,14 @@ <script> +import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import { __ } from '~/locale'; export default { + components: { + GlButton, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, props: { headerTitle: { type: String, @@ -10,29 +17,35 @@ export default { }, }, created() { - this.suggestedColors = gon.suggested_label_colors; + const rawLabelsColors = gon.suggested_label_colors; + this.suggestedColors = Object.keys(rawLabelsColors).map(colorCode => ({ + colorCode, + title: rawLabelsColors[colorCode], + })); }, }; </script> <template> <div class="dropdown-page-two dropdown-new-label"> - <div class="dropdown-title"> - <button + <div + class="dropdown-title gl-display-flex gl-justify-content-space-between gl-align-items-center" + > + <gl-button :aria-label="__('Go back')" - type="button" - class="dropdown-title-button dropdown-menu-back" - > - <i aria-hidden="true" class="fa fa-arrow-left" data-hidden="true"> </i> - </button> + category="tertiary" + class="dropdown-menu-back" + icon="arrow-left" + size="small" + /> {{ headerTitle }} - <button + <gl-button :aria-label="__('Close')" - type="button" - class="dropdown-title-button dropdown-menu-close" - > - <i aria-hidden="true" class="fa fa-times dropdown-menu-close-icon" data-hidden="true"> </i> - </button> + category="tertiary" + class="dropdown-menu-close" + icon="close" + size="small" + /> </div> <div class="dropdown-content"> <div class="dropdown-labels-error js-label-error"></div> @@ -46,10 +59,12 @@ export default { <a v-for="(color, index) in suggestedColors" :key="index" - :data-color="color" + v-gl-tooltip + :data-color="color.colorCode" :style="{ - backgroundColor: color, + backgroundColor: color.colorCode, }" + :title="color.title" href="#" > @@ -65,12 +80,12 @@ export default { /> </div> <div class="clearfix"> - <button type="button" class="btn btn-primary float-left js-new-label-btn disabled"> + <gl-button category="secondary" class="float-left js-new-label-btn disabled"> {{ __('Create') }} - </button> - <button type="button" class="btn btn-default float-right js-cancel-label-btn"> + </gl-button> + <gl-button category="secondary" class="float-right js-cancel-label-btn"> {{ __('Cancel') }} - </button> + </gl-button> </div> </div> </div> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue index eb837be165b..7b2802650a2 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue @@ -1,16 +1,22 @@ <script> -export default {}; +import { GlIcon } from '@gitlab/ui'; + +export default { + components: { + GlIcon, + }, +}; </script> <template> - <div class="dropdown-title"> - <span>{{ __('Assign labels') }}</span> + <div class="dropdown-title gl-display-flex gl-justify-content-center"> + <span class="gl-ml-auto">{{ __('Assign labels') }}</span> <button :aria-label="__('Close')" type="button" - class="dropdown-title-button dropdown-menu-close" + class="dropdown-title-button dropdown-menu-close gl-ml-auto" > - <i aria-hidden="true" class="fa fa-times dropdown-menu-close-icon" data-hidden="true"> </i> + <gl-icon name="close" aria-hidden="true" class="dropdown-menu-close-icon" /> </button> </div> </template> 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 05446903286..c2ebf78d541 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 @@ -1,4 +1,5 @@ <script> +import { GlIcon } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; import tooltip from '~/vue_shared/directives/tooltip'; @@ -6,6 +7,9 @@ export default { directives: { tooltip, }, + components: { + GlIcon, + }, props: { labels: { type: Array, @@ -49,7 +53,7 @@ export default { data-boundary="viewport" @click="handleClick" > - <i aria-hidden="true" data-hidden="true" class="fa fa-tags"> </i> + <gl-icon name="labels" /> <span>{{ labels.length }}</span> </div> </template> 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 248e9929833..34f5517ef99 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 @@ -166,7 +166,11 @@ export default { !state.showDropdownButton && !state.showDropdownContents ) { - this.handleDropdownClose(state.labels.filter(label => label.touched)); + let filterFn = label => label.touched; + if (this.isDropdownVariantEmbedded) { + filterFn = label => label.set; + } + this.handleDropdownClose(state.labels.filter(filterFn)); } }, /** @@ -186,7 +190,7 @@ export default { ].some( className => target?.classList.contains(className) || - target?.parentElement.classList.contains(className), + target?.parentElement?.classList.contains(className), ); const hadExceptionParent = ['.js-btn-back', '.js-labels-list'].some( @@ -248,10 +252,10 @@ export default { :allow-label-edit="allowLabelEdit" :labels-select-in-progress="labelsSelectInProgress" /> - <dropdown-value v-show="!showDropdownButton"> + <dropdown-value> <slot></slot> </dropdown-value> - <dropdown-button v-show="dropdownButtonVisible" /> + <dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" /> <dropdown-contents v-if="dropdownButtonVisible && showDropdownContents" ref="dropdownContents" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js index e624bd1eaee..2d236566b3d 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js @@ -54,5 +54,8 @@ export const createLabel = ({ state, dispatch }, label) => { }); }; +export const replaceSelectedLabels = ({ commit }, selectedLabels) => + commit(types.REPLACE_SELECTED_LABELS, selectedLabels); + export const updateSelectedLabels = ({ commit }, labels) => commit(types.UPDATE_SELECTED_LABELS, { labels }); diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js index 2e044dc3b3c..af92665d4eb 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js @@ -15,6 +15,7 @@ export const RECEIVE_CREATE_LABEL_FAILURE = 'RECEIVE_CREATE_LABEL_FAILURE'; export const TOGGLE_DROPDOWN_BUTTON = 'TOGGLE_DROPDOWN_VISIBILITY'; export const TOGGLE_DROPDOWN_CONTENTS = 'TOGGLE_DROPDOWN_CONTENTS'; +export const REPLACE_SELECTED_LABELS = 'REPLACE_SELECTED_LABELS'; export const UPDATE_SELECTED_LABELS = 'UPDATE_SELECTED_LABELS'; export const TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW = 'TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW'; 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..7edd290a819 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 @@ -57,6 +57,10 @@ export default { state.labelCreateInProgress = false; }, + [types.REPLACE_SELECTED_LABELS](state, selectedLabels = []) { + state.selectedLabels = selectedLabels; + }, + [types.UPDATE_SELECTED_LABELS](state, { labels }) { // Find the label to update from all the labels // and change `set` prop value to represent their current state. diff --git a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue index 148bd501a8e..135b9842cbf 100644 --- a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue @@ -1,12 +1,12 @@ <script> -import { GlNewDropdown, GlDeprecatedDropdownItem, GlSearchBoxByType, GlIcon } from '@gitlab/ui'; +import { GlDropdown, GlDeprecatedDropdownItem, GlSearchBoxByType, GlIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; export default { name: 'TimezoneDropdown', components: { - GlNewDropdown, + GlDropdown, GlDeprecatedDropdownItem, GlSearchBoxByType, GlIcon, @@ -74,7 +74,7 @@ export default { }; </script> <template> - <gl-new-dropdown :text="value" block lazy menu-class="gl-w-full!"> + <gl-dropdown :text="value" block lazy menu-class="gl-w-full!"> <template #button-content> <span class="gl-flex-grow-1" :class="{ 'gl-text-gray-300': !value }"> {{ selectedTimezoneLabel }} @@ -98,5 +98,5 @@ export default { <gl-deprecated-dropdown-item v-if="!filteredResults.length" data-testid="noMatchingResults"> {{ $options.tranlations.noResultsText }} </gl-deprecated-dropdown-item> - </gl-new-dropdown> + </gl-dropdown> </template> diff --git a/app/assets/javascripts/vue_shared/components/todo_button.vue b/app/assets/javascripts/vue_shared/components/todo_button.vue new file mode 100644 index 00000000000..debf19ccca6 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/todo_button.vue @@ -0,0 +1,28 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import { __ } from '~/locale'; + +export default { + components: { + GlButton, + }, + props: { + isTodo: { + type: Boolean, + required: false, + default: true, + }, + }, + computed: { + buttonLabel() { + return this.isTodo ? __('Mark as done') : __('Add a To-Do'); + }, + }, +}; +</script> + +<template> + <gl-button v-bind="$attrs" :aria-label="buttonLabel" @click="$emit('click', $event)"> + {{ buttonLabel }} + </gl-button> +</template> diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue index 540edc9f61c..29d4516bece 100644 --- a/app/assets/javascripts/vue_shared/components/toggle_button.vue +++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue @@ -73,7 +73,7 @@ export default { 'is-disabled': disabledInput, 'is-loading': isLoading, }" - @click="toggleFeature" + @click.prevent="toggleFeature" > <gl-loading-icon class="loading-icon" /> <span class="toggle-icon"> diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue index 4ea3d162da2..579ad53e6db 100644 --- a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue +++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue @@ -61,9 +61,9 @@ export default { v-tooltip :title="title" :data-placement="placement" - class="js-show-tooltip" + class="js-show-tooltip gl-min-w-0" > <slot></slot> </span> - <span v-else> <slot></slot> </span> + <span v-else class="gl-min-w-0"> <slot></slot> </span> </template> diff --git a/app/assets/javascripts/vue_shared/components/url_sync.vue b/app/assets/javascripts/vue_shared/components/url_sync.vue index 389d42f0829..2844d9e9e94 100644 --- a/app/assets/javascripts/vue_shared/components/url_sync.vue +++ b/app/assets/javascripts/vue_shared/components/url_sync.vue @@ -1,6 +1,6 @@ <script> import { historyPushState } from '~/lib/utils/common_utils'; -import { setUrlParams } from '~/lib/utils/url_utility'; +import { mergeUrlParams } from '~/lib/utils/url_utility'; export default { props: { @@ -14,7 +14,7 @@ export default { immediate: true, deep: true, handler(newQuery) { - historyPushState(setUrlParams(newQuery, window.location.href, true)); + historyPushState(mergeUrlParams(newQuery, window.location.href, { spreadArrays: true })); }, }, }, diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue index 699e466e848..6aaff000845 100644 --- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue +++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue @@ -1,6 +1,6 @@ <script> -import { GlPopover, GlSkeletonLoading } from '@gitlab/ui'; -import Icon from '~/vue_shared/components/icon.vue'; +/* eslint-disable vue/no-v-html */ +import { GlPopover, GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlIcon } from '@gitlab/ui'; import UserAvatarImage from '../user_avatar/user_avatar_image.vue'; import { glEmojiTag } from '../../../emoji'; @@ -10,7 +10,7 @@ export default { name: 'UserPopover', maxSkeletonLines: MAX_SKELETON_LINES, components: { - Icon, + GlIcon, GlPopover, GlSkeletonLoading, UserAvatarImage, @@ -74,16 +74,16 @@ export default { </div> <div class="gl-text-gray-500"> <div v-if="user.bio" class="gl-display-flex gl-mb-2"> - <icon name="profile" class="gl-text-gray-400 gl-flex-shrink-0" /> + <gl-icon name="profile" class="gl-text-gray-400 gl-flex-shrink-0" /> <span ref="bio" class="gl-ml-2" v-html="user.bioHtml"></span> </div> <div v-if="user.workInformation" class="gl-display-flex gl-mb-2"> - <icon name="work" class="gl-text-gray-400 gl-flex-shrink-0" /> + <gl-icon name="work" class="gl-text-gray-400 gl-flex-shrink-0" /> <span ref="workInformation" class="gl-ml-2">{{ user.workInformation }}</span> </div> </div> <div v-if="user.location" class="js-location gl-text-gray-500 gl-display-flex"> - <icon name="location" class="gl-text-gray-400 flex-shrink-0" /> + <gl-icon name="location" class="gl-text-gray-400 flex-shrink-0" /> <span class="gl-ml-2">{{ user.location }}</span> </div> <div v-if="statusHtml" class="js-user-status gl-mt-3"> diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue new file mode 100644 index 00000000000..8307c6d3b55 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue @@ -0,0 +1,118 @@ +<script> +import $ from 'jquery'; +import { __ } from '~/locale'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +import ActionsButton from '~/vue_shared/components/actions_button.vue'; + +const KEY_WEB_IDE = 'webide'; +const KEY_GITPOD = 'gitpod'; + +export default { + components: { + ActionsButton, + LocalStorageSync, + }, + props: { + webIdeUrl: { + type: String, + required: true, + }, + needsToFork: { + type: Boolean, + required: false, + default: false, + }, + showWebIdeButton: { + type: Boolean, + required: false, + default: true, + }, + showGitpodButton: { + type: Boolean, + required: false, + default: false, + }, + gitpodUrl: { + type: String, + required: false, + default: '', + }, + gitpodEnabled: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + selection: KEY_WEB_IDE, + }; + }, + computed: { + actions() { + return [this.webIdeAction, this.gitpodAction].filter(x => x); + }, + webIdeAction() { + if (!this.showWebIdeButton) { + return null; + } + + const handleOptions = this.needsToFork + ? { href: '#modal-confirm-fork', handle: () => this.showModal('#modal-confirm-fork') } + : { href: this.webIdeUrl }; + + return { + key: KEY_WEB_IDE, + text: __('Web IDE'), + secondaryText: __('Quickly and easily edit multiple files in your project.'), + tooltip: '', + attrs: { + 'data-qa-selector': 'web_ide_button', + }, + ...handleOptions, + }; + }, + gitpodAction() { + if (!this.showGitpodButton) { + return null; + } + + const handleOptions = this.gitpodEnabled + ? { href: this.gitpodUrl } + : { href: '#modal-enable-gitpod', handle: () => this.showModal('#modal-enable-gitpod') }; + + const secondaryText = __('Launch a ready-to-code development environment for your project.'); + + return { + key: KEY_GITPOD, + text: __('Gitpod'), + secondaryText, + tooltip: secondaryText, + attrs: { + 'data-qa-selector': 'gitpod_button', + }, + ...handleOptions, + }; + }, + }, + methods: { + select(key) { + this.selection = key; + }, + showModal(id) { + $(id).modal('show'); + }, + }, +}; +</script> + +<template> + <div> + <actions-button :actions="actions" :selected-key="selection" @select="select" /> + <local-storage-sync + storage-key="gl-web-ide-button-selected" + :value="selection" + @input="select" + /> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js b/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js index c628a67f7f5..be5f55a5220 100644 --- a/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js +++ b/app/assets/javascripts/vue_shared/mixins/related_issuable_mixin.js @@ -2,7 +2,6 @@ import { isEmpty } from 'lodash'; import { sprintf, __ } from '~/locale'; import { formatDate } from '~/lib/utils/datetime_utility'; import tooltip from '~/vue_shared/directives/tooltip'; -import icon from '~/vue_shared/components/icon.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; const mixins = { @@ -100,9 +99,6 @@ const mixins = { default: () => ({}), }, }, - components: { - icon, - }, directives: { tooltip, }, diff --git a/app/assets/javascripts/vuex_shared/bindings.js b/app/assets/javascripts/vuex_shared/bindings.js index edc31cfa69e..cc18b41e2de 100644 --- a/app/assets/javascripts/vuex_shared/bindings.js +++ b/app/assets/javascripts/vuex_shared/bindings.js @@ -9,7 +9,6 @@ * @param {string} root - the key of the state where to search fo they keys described in list * @returns {Object} a dictionary with all the computed properties generated */ -// eslint-disable-next-line import/prefer-default-export export const mapComputed = (list, defaultUpdateFn, root) => { const result = {}; list.forEach(item => { diff --git a/app/assets/javascripts/vuex_shared/modules/members/index.js b/app/assets/javascripts/vuex_shared/modules/members/index.js new file mode 100644 index 00000000000..ec6a94178f3 --- /dev/null +++ b/app/assets/javascripts/vuex_shared/modules/members/index.js @@ -0,0 +1,6 @@ +import createState from './state'; + +export default initialState => ({ + namespaced: true, + state: createState(initialState), +}); diff --git a/app/assets/javascripts/vuex_shared/modules/members/state.js b/app/assets/javascripts/vuex_shared/modules/members/state.js new file mode 100644 index 00000000000..1511961245c --- /dev/null +++ b/app/assets/javascripts/vuex_shared/modules/members/state.js @@ -0,0 +1,5 @@ +export default ({ members, sourceId, currentUserId }) => ({ + members, + sourceId, + currentUserId, +}); diff --git a/app/assets/javascripts/whats_new/components/app.vue b/app/assets/javascripts/whats_new/components/app.vue index d974556cb9e..a00661c214d 100644 --- a/app/assets/javascripts/whats_new/components/app.vue +++ b/app/assets/javascripts/whats_new/components/app.vue @@ -1,16 +1,40 @@ <script> import { mapState, mapActions } from 'vuex'; -import { GlDrawer } from '@gitlab/ui'; +import { GlDrawer, GlBadge, GlIcon, GlLink } from '@gitlab/ui'; export default { components: { GlDrawer, + GlBadge, + GlIcon, + GlLink, + }, + props: { + features: { + type: String, + required: false, + default: null, + }, }, computed: { ...mapState(['open']), + parsedFeatures() { + let features; + + try { + features = JSON.parse(this.$props.features) || []; + } catch (err) { + features = []; + } + + return features; + }, + }, + mounted() { + this.openDrawer(); }, methods: { - ...mapActions(['closeDrawer']), + ...mapActions(['openDrawer', 'closeDrawer']), }, }; </script> @@ -19,11 +43,31 @@ export default { <div> <gl-drawer class="mt-6" :open="open" @close="closeDrawer"> <template #header> - <h4>{{ __("What's new at GitLab") }}</h4> - </template> - <template> - <div></div> + <h4 class="page-title my-2">{{ __("What's new at GitLab") }}</h4> </template> + <div class="pb-6"> + <div v-for="feature in parsedFeatures" :key="feature.title" class="mb-6"> + <gl-link :href="feature.url" target="_blank"> + <h5 class="gl-font-base">{{ feature.title }}</h5> + </gl-link> + <div class="mb-2"> + <template v-for="package_name in feature.packages"> + <gl-badge :key="package_name" size="sm" class="whats-new-item-badge mr-1"> + <gl-icon name="license" />{{ package_name }} + </gl-badge> + </template> + </div> + <gl-link :href="feature.url" target="_blank"> + <img + :alt="feature.title" + :src="feature.image_url" + class="img-thumbnail px-6 py-2 whats-new-item-image" + /> + </gl-link> + <p class="pt-2">{{ feature.body }}</p> + <gl-link :href="feature.url" target="_blank">{{ __('Learn more') }}</gl-link> + </div> + </div> </gl-drawer> </div> </template> diff --git a/app/assets/javascripts/whats_new/components/trigger.vue b/app/assets/javascripts/whats_new/components/trigger.vue deleted file mode 100644 index e6c48e92888..00000000000 --- a/app/assets/javascripts/whats_new/components/trigger.vue +++ /dev/null @@ -1,19 +0,0 @@ -<script> -import { mapActions } from 'vuex'; -import { GlButton } from '@gitlab/ui'; - -export default { - components: { - GlButton, - }, - methods: { - ...mapActions(['openDrawer']), - }, -}; -</script> - -<template> - <li> - <gl-button variant="link" @click="openDrawer">{{ __("See what's new at GitLab") }}</gl-button> - </li> -</template> diff --git a/app/assets/javascripts/whats_new/index.js b/app/assets/javascripts/whats_new/index.js index c9ee3404d2a..19cdb590ae2 100644 --- a/app/assets/javascripts/whats_new/index.js +++ b/app/assets/javascripts/whats_new/index.js @@ -1,32 +1,28 @@ import Vue from 'vue'; import App from './components/app.vue'; -import Trigger from './components/trigger.vue'; import store from './store'; -export default () => { - // eslint-disable-next-line no-new - new Vue({ - el: document.getElementById('whats-new-app'), - store, - components: { - App, - }, - - render(createElement) { - return createElement('app'); - }, - }); +let whatsNewApp; - // eslint-disable-next-line no-new - new Vue({ - el: document.getElementById('whats-new-trigger'), - store, - components: { - Trigger, - }, +export default () => { + if (whatsNewApp) { + store.dispatch('openDrawer'); + } else { + const whatsNewElm = document.getElementById('whats-new-app'); - render(createElement) { - return createElement('trigger'); - }, - }); + whatsNewApp = new Vue({ + el: whatsNewElm, + store, + components: { + App, + }, + render(createElement) { + return createElement('app', { + props: { + features: whatsNewElm.getAttribute('data-features'), + }, + }); + }, + }); + } }; diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss new file mode 100644 index 00000000000..f706b615e7e --- /dev/null +++ b/app/assets/stylesheets/_page_specific_files.scss @@ -0,0 +1,65 @@ +@import './pages/admin'; +@import './pages/alert_management/details'; +@import './pages/alert_management/severity-icons'; +@import './pages/boards'; +@import './pages/branches'; +@import './pages/builds'; +@import './pages/ci_projects'; +@import './pages/clusters'; +@import './pages/commits'; +@import './pages/cycle_analytics'; +@import './pages/deploy_keys'; +@import './pages/detail_page'; +@import './pages/dev_ops_report'; +@import './pages/diff'; +@import './pages/editor'; +@import './pages/environment_logs'; +@import './pages/environments'; +@import './pages/error_details'; +@import './pages/error_list'; +@import './pages/error_tracking_list'; +@import './pages/events'; +@import './pages/experience_level'; +@import './pages/experimental_separate_sign_up'; +@import './pages/graph'; +@import './pages/groups'; +@import './pages/help'; +@import './pages/import'; +@import './pages/incident_management_list'; +@import './pages/issuable'; +@import './pages/issues/issue_count_badge'; +@import './pages/issues/issues_list'; +@import './pages/issues'; +@import './pages/labels'; +@import './pages/login'; +@import './pages/members'; +@import './pages/merge_conflicts'; +@import './pages/merge_requests'; +@import './pages/milestone'; +@import './pages/monitor'; +@import './pages/note_form'; +@import './pages/notes'; +@import './pages/notifications'; +@import './pages/pages'; +@import './pages/pipeline_schedules'; +@import './pages/pipelines'; +@import './pages/profile'; +@import './pages/profiles/preferences'; +@import './pages/projects'; +@import './pages/prometheus'; +@import './pages/reports'; +@import './pages/runners'; +@import './pages/search'; +@import './pages/serverless'; +@import './pages/service_desk'; +@import './pages/settings'; +@import './pages/settings_ci_cd'; +@import './pages/sherlock'; +@import './pages/status'; +@import './pages/storage_quota'; +@import './pages/tags'; +@import './pages/tree'; +@import './pages/trials'; +@import './pages/ui_dev_kit'; +@import './pages/users'; +@import './pages/wiki'; diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index f5393ef47d6..8acd338fff8 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -13,7 +13,7 @@ // directory. @import '@gitlab/at.js/dist/css/jquery.atwho'; @import 'dropzone/dist/basic'; -@import 'select2/select2'; +@import 'select2'; // GitLab UI framework @import 'framework'; @@ -22,7 +22,7 @@ @import 'fontawesome_custom'; // Page specific styles (issues, projects etc): -@import 'pages/**/*'; +@import 'page_specific_files'; // Component specific styles, will be moved to gitlab-ui @import 'components/**/*'; diff --git a/app/assets/stylesheets/components/batch_comments/review_bar.scss b/app/assets/stylesheets/components/batch_comments/review_bar.scss new file mode 100644 index 00000000000..76bf7ac81e8 --- /dev/null +++ b/app/assets/stylesheets/components/batch_comments/review_bar.scss @@ -0,0 +1,122 @@ +.review-bar-component { + position: fixed; + bottom: 0; + left: 0; + width: 100%; + background: $white; + z-index: 300; + padding: 7px 0 6px; // to keep aligned with "collapse sidebar" button on the left sidebar + border-top: 1px solid $border-color; + padding-left: $contextual-sidebar-width; + padding-right: $gutter_collapsed_width; + transition: padding $sidebar-transition-duration; + + .page-with-icon-sidebar & { + padding-left: $contextual-sidebar-collapsed-width; + } + + .right-sidebar-expanded & { + padding-right: $gutter_width; + } + + @media (max-width: map-get($grid-breakpoints, sm)-1) { + padding-left: 0; + padding-right: 0; + } + + .dropdown { + margin-left: $grid-size; + } +} + +.review-bar-content { + max-width: $limited-layout-width; + padding: 0 $gl-padding; + width: 100%; + margin: 0 auto; +} + +.review-preview-dropdown { + .review-preview-item.menu-item { + display: flex; + flex-wrap: wrap; + padding: 8px 16px; + cursor: pointer; + + &:not(.is-last) { + border-bottom: 1px solid $list-border; + } + } + + .dropdown-menu { + top: auto; + bottom: 36px; + + &.show { + max-height: 400px; + + @include media-breakpoint-down(xs) { + width: calc(100vw - 32px); + } + } + } + + .dropdown-content { + max-height: 300px; + } + + .dropdown-title { + padding: $grid-size 25px $gl-padding; + margin-bottom: 0; + } + + .dropdown-footer { + margin-top: 0; + } + + .dropdown-menu-close { + top: 6px; + } +} + +.review-preview-dropdown-toggle { + svg.s16 { + width: 15px; + height: 15px; + margin-top: -1px; + top: 3px; + margin-left: 4px; + } +} + +.review-preview-item-header { + display: flex; + align-items: center; + width: 100%; + margin-bottom: 4px; + + > .bold { + display: flex; + min-width: 0; + line-height: 16px; + } +} + +.review-preview-item-footer { + display: flex; + align-items: center; + margin-top: 4px; +} + +.review-preview-item-content { + width: 100%; + + p { + display: block; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-bottom: 0; + } +} diff --git a/app/assets/stylesheets/components/design_management/design.scss b/app/assets/stylesheets/components/design_management/design.scss index 80421598966..21133316291 100644 --- a/app/assets/stylesheets/components/design_management/design.scss +++ b/app/assets/stylesheets/components/design_management/design.scss @@ -34,6 +34,10 @@ background-color: $gray-500; } } + + .design-detail-overlay-add-comment { + cursor: crosshair; + } } .design-presentation-wrapper { diff --git a/app/assets/stylesheets/components/related_items_list.scss b/app/assets/stylesheets/components/related_items_list.scss index c0699844387..7c668666d70 100644 --- a/app/assets/stylesheets/components/related_items_list.scss +++ b/app/assets/stylesheets/components/related_items_list.scss @@ -48,7 +48,7 @@ $item-remove-button-space: 42px; } .confidential-icon { - color: $orange-600; + color: $orange-500; } .item-title-wrapper { @@ -123,21 +123,11 @@ $item-remove-button-space: 42px; .item-milestone { text-decoration: none; max-width: $item-milestone-max-width; - - .ic-clock { - margin-right: $gl-padding-4; - } } .item-weight { max-width: $item-weight-max-width; } - - .item-milestone .ic-clock, - .item-weight .ic-weight, - .item-due-date .ic-calendar { - color: $gl-text-color-secondary; - } } .item-assignees { diff --git a/app/assets/stylesheets/components/rich_content_editor.scss b/app/assets/stylesheets/components/rich_content_editor.scss index ade1bb2099d..b1189137d59 100644 --- a/app/assets/stylesheets/components/rich_content_editor.scss +++ b/app/assets/stylesheets/components/rich_content_editor.scss @@ -6,7 +6,7 @@ // Toolbar buttons .tui-editor-defaultUI-toolbar .toolbar-button { - color: $gl-gray-600; + color: $gray-500; border: 0; &:hover, diff --git a/app/assets/stylesheets/components/whats_new.scss b/app/assets/stylesheets/components/whats_new.scss new file mode 100644 index 00000000000..4fff900f5a5 --- /dev/null +++ b/app/assets/stylesheets/components/whats_new.scss @@ -0,0 +1,9 @@ +.gl-badge.whats-new-item-badge { + background-color: $purple-light; + color: $purple; + font-weight: bold; +} + +.whats-new-item-image { + border-color: $gray-50; +} diff --git a/app/assets/stylesheets/fontawesome_custom.scss b/app/assets/stylesheets/fontawesome_custom.scss index a2117e9c012..30d56d99e1c 100644 --- a/app/assets/stylesheets/fontawesome_custom.scss +++ b/app/assets/stylesheets/fontawesome_custom.scss @@ -84,10 +84,6 @@ color: $white; } -.fa-question-circle::before { - content: '\f059'; -} - .fa-chevron-down::before { content: '\f078'; } @@ -130,18 +126,10 @@ content: '\f101'; } -.fa-trash::before { - content: '\f1f8'; -} - .fa-angle-double-left::before { content: '\f100'; } -.fa-arrow-left::before { - content: '\f060'; -} - .fa-trash-o::before { content: '\f014'; } @@ -170,14 +158,6 @@ content: '\f0c6'; } -.fa-tag::before { - content: '\f02b'; -} - -.fa-arrow-up::before { - content: '\f062'; -} - .fa-bug::before { content: '\f188'; } @@ -186,10 +166,6 @@ content: '\f1a0'; } -.fa-user::before { - content: '\f007'; -} - .fa-exclamation-circle::before { content: '\f06a'; } @@ -198,10 +174,6 @@ content: '\f0f3'; } -.fa-arrow-down::before { - content: '\f063'; -} - .fa-bitbucket-square::before { content: '\f172'; } @@ -210,14 +182,6 @@ content: '\f016'; } -.fa-users::before { - content: '\f0c0'; -} - -.fa-tags::before { - content: '\f02c'; -} - .fa-lightbulb-o::before { content: '\f0eb'; } @@ -250,10 +214,6 @@ content: '\f06d'; } -.fa-download::before { - content: '\f019'; -} - .fa-globe::before { content: '\f0ac'; } @@ -266,14 +226,6 @@ content: '\f04b'; } -.fa-arrow-right::before { - content: '\f061'; -} - -.fa-user-secret::before { - content: '\f21b'; -} - .fa-search-plus::before { content: '\f00e'; } diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 413e0dde535..f875420b9c9 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -23,7 +23,6 @@ @import 'framework/flash'; @import 'framework/forms'; @import 'framework/gfm'; -@import 'framework/gitlab_theme'; @import 'framework/header'; @import 'framework/highlight'; @import 'framework/issue_box'; diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 893a494d240..a9c1652d00d 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -112,7 +112,7 @@ } @mixin btn-orange { - @include btn-color($orange-500, $orange-600, $orange-600, $orange-700, $orange-700, $orange-800, $white); + @include btn-color($orange-500, $orange-600, $orange-500, $orange-600, $orange-600, $orange-800, $white); } @mixin btn-red { @@ -182,7 +182,7 @@ } &.btn-warning { - @include btn-outline($white, $orange-500, $orange-500, $orange-100, $orange-700, $orange-500, $orange-200, $orange-600, $orange-800); + @include btn-outline($white, $orange-500, $orange-500, $orange-50, $orange-600, $orange-600, $orange-100, $orange-700, $orange-700); } &.btn-primary, @@ -202,7 +202,7 @@ &.btn-close, &.btn-close-color { - @include btn-outline($white, $orange-600, $orange-500, $orange-100, $orange-700, $orange-500, $orange-200, $orange-600, $orange-800); + @include btn-outline($white, $orange-500, $orange-500, $orange-50, $orange-600, $orange-600, $orange-100, $orange-700, $orange-700); } &.btn-spam { @@ -229,7 +229,7 @@ } &.btn-icon { - color: $gl-gray-700; + color: $gray-700; } .fa-caret-down, @@ -394,7 +394,7 @@ } .clone-dropdown-btn a { - color: $gl-gray-700; + color: $gray-700; &:hover { text-decoration: none; @@ -542,3 +542,13 @@ fieldset[disabled] .btn, .btn-no-padding { padding: 0; } + +// This class helps convert `.gl-button` children so that they consistently +// match the style of `.btn` elements which might be around them. Ideally we +// wouldn't need this class. +// +// Remove by upgrading all buttons in a container to use the new `.gl-button` style. +.gl-button-deprecated-adapter .gl-button { + box-shadow: none; + border-width: 1px; +} diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss index c5bb2a1256a..d0d2328ea98 100644 --- a/app/assets/stylesheets/framework/callout.scss +++ b/app/assets/stylesheets/framework/callout.scss @@ -37,12 +37,12 @@ } .bs-callout-warning { - background-color: $orange-100; + background-color: $orange-50; border-color: $orange-200; - color: $orange-900; + color: $gray-900; a { - color: $orange-900; + color: $blue-600; } } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 00679cf20fa..714ef8b2175 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -74,7 +74,7 @@ .hint { font-style: italic; - color: $gl-gray-200; + color: $gray-200; } .light { color: $gl-text-color; } @@ -162,13 +162,13 @@ table { .loading { margin: 20px auto; height: 40px; - color: $gl-gray-700; + color: $gray-700; font-size: 32px; text-align: center; } p.time { - color: $gl-gray-200; + color: $gray-200; font-size: 90%; margin: 30px 3px 3px 2px; } @@ -237,7 +237,7 @@ li.note { } .warning_message { - @include message($orange-100, $orange-200, $orange-800); + @include message($orange-50, $orange-200, $gray-900); } .danger_message { @@ -246,7 +246,7 @@ li.note { .gitlab-promo { a { - color: $gl-gray-350; + color: $gray-300; margin-right: 30px; } } @@ -416,7 +416,6 @@ img.emoji { .flex-no-shrink { flex-shrink: 0; } .ws-initial { white-space: initial; } .ws-normal { white-space: normal; } -.ws-pre-wrap { white-space: pre-wrap; } .overflow-auto { overflow: auto; } .overflow-visible { overflow: visible; } @@ -454,35 +453,6 @@ img.emoji { } } -/** COMMON SPACING CLASSES **/ -/** - 🚨 Do not use these classes — they are deprecated and being removed. 🚨 - See https://gitlab.com/gitlab-org/gitlab/issues/36857 for more details. - - Instead, if you need a spacing class, please use one from Gitlab UI — - https://unpkg.com/browse/@gitlab/ui/src/scss/utilities.scss — which uses the following scale. - $gl-spacing-scale-0: 0; - $gl-spacing-scale-1: 2px; - $gl-spacing-scale-2: 4px; - $gl-spacing-scale-3: 8px; - $gl-spacing-scale-4: 12px; - $gl-spacing-scale-5: 16px; - $gl-spacing-scale-6: 24px; - $gl-spacing-scale-7: 32px; - $gl-spacing-scale-8: 40px; - $gl-spacing-scale-9: 48px; - $gl-spacing-scale-10: 56px; - $gl-spacing-scale-11: 64px; - $gl-spacing-scale-12: 80px; - $gl-spacing-scale-13: 96px; -**/ -@each $index, $padding in $spacing-scale { - #{'.gl-p-#{$index}-deprecated-no-really-do-not-use-me'} { padding: $padding; } - #{'.gl-pl-#{$index}-deprecated-no-really-do-not-use-me'} { padding-left: $padding; } - #{'.gl-pr-#{$index}-deprecated-no-really-do-not-use-me'} { padding-right: $padding; } - #{'.gl-pt-#{$index}-deprecated-no-really-do-not-use-me'} { padding-top: $padding; } - #{'.gl-pb-#{$index}-deprecated-no-really-do-not-use-me'} { padding-bottom: $padding; } -} /** * Removes browser specific clear icon from input fields in @@ -557,4 +527,3 @@ img.emoji { See https://gitlab.com/gitlab-org/gitlab/issues/36857 for more details. **/ .gl-line-height-14 { line-height: $gl-line-height-14; } -.gl-font-size-20 { font-size: $gl-font-size-20; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 6b742853f8f..ad5864ef6d9 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -164,14 +164,20 @@ right: 8px; } - .ic-chevron-down { + .dropdown-menu-toggle-icon { position: absolute; - top: $gl-padding-8; right: $gl-padding-8; color: $gray-darkest; } } +.labels { + // Prevent double scroll-bars for labels dropdown. + .dropdown-menu-toggle.wide + .dropdown-select { + max-height: unset; + } +} + .gl-dropdown .dropdown-menu-toggle { padding-right: $gl-padding-8; @@ -410,7 +416,7 @@ flex-direction: column; .reference { - color: $gl-gray-400; + color: $gray-300; margin-top: $gl-padding-4; } } @@ -590,11 +596,8 @@ } .dropdown-title-button { - position: absolute; - top: 0; padding: 0; color: $dropdown-title-btn-color; - font-size: 14px; border: 0; background: none; outline: 0; @@ -604,20 +607,9 @@ } } -.dropdown-menu-close { - top: $gl-padding-4; - right: $gl-padding-8; - width: 20px; - height: 20px; -} - -.dropdown-menu-close-icon { - vertical-align: middle; -} - .dropdown-menu-back { - left: 7px; - top: 2px; + left: 10px; + top: $gl-padding-8; } .dropdown-input { @@ -627,7 +619,8 @@ .fa, .input-icon, - .ic-search { + .dropdown-input-clear, + .dropdown-input-search { position: absolute; top: $gl-padding-8; right: 20px; @@ -666,25 +659,27 @@ width: 100%; min-height: 30px; padding: 0 7px; - color: $gl-gray-700; + color: $gray-700; line-height: 30px; border: 1px solid $dropdown-divider-bg; border-radius: 2px; outline: 0; &:focus { - color: $gl-gray-700; + color: $gray-700; border-color: $blue-300; box-shadow: 0 0 4px $dropdown-input-focus-shadow; - ~ .fa { - color: $gl-gray-700; + ~ .fa, + ~ .dropdown-input-clear { + color: $gray-700; } } &:hover { - ~ .fa { - color: $gl-gray-700; + ~ .fa, + ~ .dropdown-input-clear { + color: $gray-700; } } } @@ -1070,7 +1065,7 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu { color: $dropdown-title-btn-color; &:hover { - color: $gl-gray-400; + color: $gray-300; } } } @@ -1105,3 +1100,42 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu { } } } + +// This class won't be needed once we can directly add utility classes to the child +// https://github.com/bootstrap-vue/bootstrap-vue/issues/5669 +.gl-dropdown-text-py-0 { + .b-dropdown-text { + padding-top: 0; + padding-bottom: 0; + } +} + +// This class won't be needed once we can directly add utility classes to the child +// https://github.com/bootstrap-vue/bootstrap-vue/issues/5669 +.gl-dropdown-text-block { + .b-dropdown-text { + display: block; + } +} + +// This class won't be needed once we can add a prop for this in the GitLab UI component +// https://gitlab.com/gitlab-org/gitlab-ui/-/issues/966 +.gl-new-dropdown { + .gl-dropdown-menu-wide { + width: $gl-dropdown-width-wide; + } +} + +.gl-dropdown-item-deprecated-adapter { + .dropdown-item { + align-items: flex-start; + + .gl-new-dropdown-item-text-primary { + @include gl-font-weight-bold; + } + + .gl-new-dropdown-item-text-secondary { + color: inherit; + } + } +} diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index ef7d39a5e7e..76c6e03377c 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -183,7 +183,7 @@ &.line-numbers { float: none; - border-left: 1px solid $gl-gray-100; + border-left: 1px solid $gray-100; i { float: none; @@ -300,7 +300,7 @@ span.idiff { .renamed-file { a { - color: $orange-600; + color: $orange-500; } } @@ -495,9 +495,12 @@ span.idiff { max-height: 20rem; } -#js-openapi-viewer pre.version { - background-color: transparent; - border: transparent; +#js-openapi-viewer { + pre.version, + code { + background-color: transparent; + border: transparent; + } } .code-navigation-line:hover { diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index ed4281123cd..1a394ad124b 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -243,7 +243,7 @@ } } - .fa-times { + .clear-search-icon { right: 10px; color: $gray-darkest; } @@ -255,7 +255,7 @@ outline: none; z-index: 1; - &:hover .fa-times { + &:hover .clear-search-icon { color: $common-gray-dark; } } @@ -348,11 +348,11 @@ } @include media-breakpoint-down(sm) { - .issues-details-filters { - padding-top: 0; - padding-bottom: 0; + .issues-details-filters, + .epics-details-filters { + padding-top: $gl-padding-8; + padding-bottom: $gl-padding-8; background-color: $white; - border-top: 0; } .boards-switcher { diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index d604d97d270..0fb91db0afb 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -65,8 +65,8 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25); } .flash-warning { - background-color: $orange-100; - color: $orange-800; + background-color: $orange-50; + color: $gray-900; cursor: default; } diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss index fbafb22cf37..579a68ac8e4 100644 --- a/app/assets/stylesheets/framework/gfm.scss +++ b/app/assets/stylesheets/framework/gfm.scss @@ -13,7 +13,7 @@ border-radius: $border-radius-default; &.current-user { - background-color: $orange-100; + background-color: $orange-50; } } diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss deleted file mode 100644 index 97bd6ca6fe2..00000000000 --- a/app/assets/stylesheets/framework/gitlab_theme.scss +++ /dev/null @@ -1,431 +0,0 @@ -/** - * Styles the GitLab application with a specific color theme - */ - -@mixin gitlab-theme( - $search-and-nav-links, - $active-tab-border, - $border-and-box-shadow, - $sidebar-text, - $nav-svg-color, - $color-alternate -) { - // Header - - .navbar-gitlab { - background-color: $nav-svg-color; - - .navbar-collapse { - color: $search-and-nav-links; - } - - .container-fluid { - .navbar-toggler { - border-left: 1px solid lighten($border-and-box-shadow, 10%); - - svg { - fill: $search-and-nav-links; - } - } - } - - .navbar-sub-nav, - .navbar-nav { - > li { - > a, - > button { - &:hover, - &:focus { - background-color: rgba($search-and-nav-links, 0.2); - } - } - - &.active, - &.dropdown.show { - > a, - > button { - color: $nav-svg-color; - background-color: $color-alternate; - } - } - - &.line-separator { - border-left: 1px solid rgba($search-and-nav-links, 0.2); - } - } - } - - .navbar-sub-nav { - color: $search-and-nav-links; - } - - .nav { - > li { - color: $search-and-nav-links; - - > a { - &.header-user-dropdown-toggle { - .header-user-avatar { - border-color: $search-and-nav-links; - } - - .header-user-notification-dot { - border: 2px solid $nav-svg-color; - } - } - - &:hover, - &:focus { - @include media-breakpoint-up(sm) { - background-color: rgba($search-and-nav-links, 0.2); - } - - svg { - fill: currentColor; - } - - &.header-user-dropdown-toggle .header-user-notification-dot { - border-color: $nav-svg-color + 33; - } - } - } - - &.active > a, - &.dropdown.show > a { - color: $nav-svg-color; - background-color: $color-alternate; - - &:hover { - svg { - fill: $nav-svg-color; - } - } - - &.header-user-dropdown-toggle .header-user-notification-dot { - border-color: $white; - } - } - - .impersonated-user, - .impersonated-user:hover { - svg { - fill: $nav-svg-color; - } - } - } - } - } - - .navbar .title { - > a { - &:hover, - &:focus { - background-color: rgba($search-and-nav-links, 0.2); - } - } - } - - .search { - form { - background-color: rgba($search-and-nav-links, 0.2); - - &:hover { - background-color: rgba($search-and-nav-links, 0.3); - } - } - - .search-input::placeholder { - color: rgba($search-and-nav-links, 0.8); - } - - .search-input-wrap { - .search-icon, - .clear-icon { - fill: rgba($search-and-nav-links, 0.8); - } - } - - &.search-active { - form { - background-color: $white; - } - - .search-input-wrap { - .search-icon { - fill: rgba($search-and-nav-links, 0.8); - } - } - } - } - - // Sidebar - .nav-sidebar li.active { - box-shadow: inset 4px 0 0 $border-and-box-shadow; - - > a { - color: $sidebar-text; - } - - .nav-icon-container svg { - fill: $sidebar-text; - } - } - - .sidebar-top-level-items > li.active .badge.badge-pill { - color: $sidebar-text; - } - - .nav-links li { - &.active a, - &.md-header-tab.active button, - a.active { - border-bottom: 2px solid $active-tab-border; - - .badge.badge-pill { - font-weight: $gl-font-weight-bold; - } - } - } - - .branch-header-title { - color: $border-and-box-shadow; - } - - .ide-sidebar-link { - &.active { - color: $border-and-box-shadow; - box-shadow: inset 3px 0 $border-and-box-shadow; - - &.is-right { - box-shadow: inset -3px 0 $border-and-box-shadow; - } - } - } -} - -body { - &.ui-indigo { - @include gitlab-theme( - $indigo-200, - $indigo-500, - $indigo-700, - $indigo-800, - $indigo-900, - $white - ); - } - - &.ui-light-indigo { - @include gitlab-theme( - $indigo-200, - $indigo-500, - $indigo-500, - $indigo-700, - $indigo-700, - $white - ); - } - - &.ui-blue { - @include gitlab-theme( - $theme-blue-200, - $theme-blue-500, - $theme-blue-700, - $theme-blue-800, - $theme-blue-900, - $white - ); - } - - &.ui-light-blue { - @include gitlab-theme( - $theme-light-blue-200, - $theme-light-blue-500, - $theme-light-blue-500, - $theme-light-blue-700, - $theme-light-blue-700, - $white - ); - } - - &.ui-green { - @include gitlab-theme( - $theme-green-200, - $theme-green-500, - $theme-green-700, - $theme-green-800, - $theme-green-900, - $white - ); - } - - &.ui-light-green { - @include gitlab-theme( - $theme-green-200, - $theme-green-500, - $theme-green-500, - $theme-light-green-700, - $theme-light-green-700, - $white - ); - } - - &.ui-red { - @include gitlab-theme( - $theme-red-200, - $theme-red-500, - $theme-red-700, - $theme-red-800, - $theme-red-900, - $white - ); - } - - &.ui-light-red { - @include gitlab-theme( - $theme-light-red-200, - $theme-light-red-500, - $theme-light-red-500, - $theme-light-red-700, - $theme-light-red-700, - $white - ); - } - - &.ui-dark { - @include gitlab-theme( - $gray-200, - $gray-300, - $gray-500, - $gray-700, - $gray-900, - $white - ); - } - - &.ui-light { - @include gitlab-theme( - $gray-500, - $gray-700, - $gray-500, - $gray-500, - $gray-50, - $gray-500 - ); - - .navbar-gitlab { - background-color: $gray-50; - box-shadow: 0 1px 0 0 $border-color; - - .logo-text svg { - fill: $gray-900; - } - - .navbar-sub-nav, - .navbar-nav { - > li { - > a:hover, - > a:focus, - > button:hover { - color: $gray-900; - } - - &.active > a, - &.active > a:hover, - &.active > button { - color: $white; - } - } - } - - .container-fluid { - .navbar-toggler, - .navbar-toggler:hover { - color: $gray-500; - border-left: 1px solid $gray-100; - } - } - } - - .search { - form { - background-color: $white; - box-shadow: inset 0 0 0 1px $border-color; - - &:hover { - background-color: $white; - box-shadow: inset 0 0 0 1px $blue-200; - } - } - - .search-input-wrap { - .search-icon { - fill: $gray-100; - } - - .search-input { - color: $gl-text-color; - } - } - } - - .nav-sidebar li.active { - > a { - color: $gray-900; - } - - svg { - fill: $gray-900; - } - } - - .sidebar-top-level-items > li.active .badge.badge-pill { - color: $gray-900; - } - } - - &.gl-dark { - .logo-text svg { - fill: $gl-text-color; - } - - .navbar-gitlab { - background-color: $gray-50; - - .navbar-sub-nav, - .navbar-nav { - li { - > a:hover, - > a:focus, - > button:hover, - > button:focus { - color: $gl-text-color; - background-color: $gray-200; - } - } - - li.active, - li.dropdown.show { - > a, - > button { - color: $gl-text-color; - background-color: $gray-200; - } - } - } - - .search { - form { - background-color: $gray-100; - box-shadow: inset 0 0 0 1px $border-color; - - &:active, - &:hover { - background-color: $gray-100; - box-shadow: inset 0 0 0 1px $blue-200; - } - } - } - } - } -} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 50628c7de82..cf21c23cb17 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -235,9 +235,8 @@ border-top-left-radius: 0; border-bottom-left-radius: 0; - i { + svg { color: $orange-500; - font-size: 20px; } } } @@ -459,15 +458,15 @@ box-shadow: 0 1px 0 rgba($gl-header-color, 0.2); &.green-badge { - background-color: $green-500; + background-color: $green-400; } &.merge-requests-count { - background-color: $orange-600; + background-color: $orange-400; } &.todos-count { - background-color: $blue-500; + background-color: $blue-400; } } diff --git a/app/assets/stylesheets/framework/job_log.scss b/app/assets/stylesheets/framework/job_log.scss index 2448be1bca3..d48a5116677 100644 --- a/app/assets/stylesheets/framework/job_log.scss +++ b/app/assets/stylesheets/framework/job_log.scss @@ -17,7 +17,7 @@ } .line-number { - color: $gl-gray-500; + color: $gray-500; padding: 0 $gl-padding-8; min-width: $job-line-number-width; margin-left: -$job-line-number-margin; @@ -28,7 +28,7 @@ &:active, &:visited { text-decoration: underline; - color: $gl-gray-500; + color: $gray-500; } } @@ -43,7 +43,7 @@ } .log-duration-badge { - background: $gl-gray-400; + background: $gray-300; } .loader-animation { diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 738150dbd2e..2464ea3607b 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -34,17 +34,6 @@ } } - &.warning-row { - background-color: $orange-100; - border-color: $orange-200; - color: $orange-700; - - &:hover { - background: $orange-100; - } - - } - &.smoke { background-color: $gray-light; } &:last-child { @@ -132,10 +121,10 @@ ul.content-list { a { color: $gl-text-color; - } - .member-group-link { - color: $blue-600; + &.inline-link { + color: $blue-600; + } } .description { diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss index 1352fa13e1a..292d57f132c 100644 --- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss +++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss @@ -97,13 +97,8 @@ } /* Small devices (phones, tablets, 768px and lower) */ - @include media-breakpoint-down(xs) { + @include media-breakpoint-down(sm) { width: 100%; - - &.mobile-separator { - border-bottom: 1px solid $border-color; - margin-bottom: $gl-padding-8; - } } } diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index e81ecfb43d5..86a5aa1a16e 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -300,7 +300,7 @@ } .group-path { - color: $gl-gray-400; + color: $gray-300; } } @@ -310,7 +310,7 @@ } .project-path { - color: $gl-gray-400; + color: $gray-300; } } @@ -332,7 +332,7 @@ .namespace-result { .namespace-kind { - color: $gl-gray-350; + color: $gray-300; font-weight: $gl-font-weight-normal; } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 4ba9db811b7..d867cc96dbc 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -145,6 +145,13 @@ .value.dont-hide ~ .selectbox { padding-top: $gl-padding-8; } + + // This is for sidebar components using gl-button for the Edit button to be consistent with the + // rest of the sidebar, and could be removed once the sidebar has been fully converted to use + // gitlab-ui components. + .title .gl-button { + color: $gl-text-color; + } } .pikaday-container { diff --git a/app/assets/stylesheets/framework/spinner.scss b/app/assets/stylesheets/framework/spinner.scss index d734895c7dc..a74aeb9f220 100644 --- a/app/assets/stylesheets/framework/spinner.scss +++ b/app/assets/stylesheets/framework/spinner.scss @@ -31,7 +31,7 @@ border-style: solid; display: inline-flex; @include spinner-size(16px, 2px); - @include spinner-color($orange-600); + @include spinner-color($orange-400); &.spinner-md { @include spinner-size(32px, 3px); diff --git a/app/assets/stylesheets/framework/stacked_progress_bar.scss b/app/assets/stylesheets/framework/stacked_progress_bar.scss index a3037549881..37283db8b71 100644 --- a/app/assets/stylesheets/framework/stacked_progress_bar.scss +++ b/app/assets/stylesheets/framework/stacked_progress_bar.scss @@ -37,7 +37,7 @@ .status-neutral { background-color: $gray-100; - color: $gl-gray-dark; + color: $gray-900; &:hover { background-color: $gray-200; diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 1f60485aa36..59e83608d9d 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -180,3 +180,37 @@ table { border-top: 0; } } + +.vulnerability-list { + @media (min-width: $breakpoint-sm) { + .checkbox { + padding-left: $gl-spacing-scale-4; + padding-right: 0; + + + td, + + th { + padding-left: $gl-spacing-scale-4; + } + } + + .detected { + width: 9%; + } + + .status { + width: 8%; + } + + .severity { + width: 10%; + } + + .identifier { + width: 16%; + } + + .scanner { + width: 15%; + } + } +} diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 8758fe15870..8b5fa6c1b6c 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -84,7 +84,7 @@ padding: 3px 5px; font-size: 11px; line-height: 10px; - color: $gl-gray-700; + color: $gray-700; vertical-align: middle; background-color: $gray-10; border-width: 1px; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 69e00f9b2c4..8cebfc430e0 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -104,15 +104,6 @@ $t-gray-a-04: rgba($black, 0.04) !default; $t-gray-a-06: rgba($black, 0.06) !default; $t-gray-a-08: rgba($black, 0.08) !default; -$gl-gray-100: #ddd !default; -$gl-gray-200: #ccc !default; -$gl-gray-350: #aaa !default; -$gl-gray-400: #999 !default; -$gl-gray-500: #777 !default; -$gl-gray-600: #666 !default; -$gl-gray-700: #555 !default; -$gl-gray-800: #333 !default; - $green-50: #ecf4ee !default; $green-100: #c3e6cd !default; $green-200: #91d4a8 !default; @@ -137,17 +128,17 @@ $blue-800: #064787 !default; $blue-900: #033464 !default; $blue-950: #002850 !default; -$orange-50: #fffaf4 !default; -$orange-100: #fff1de !default; -$orange-200: #fed69f !default; -$orange-300: #fdbc60 !default; -$orange-400: #fca429 !default; -$orange-500: #fc9403 !default; -$orange-600: #de7e00 !default; -$orange-700: #c26700 !default; -$orange-800: #a35200 !default; -$orange-900: #853c00 !default; -$orange-950: #592800 !default; +$orange-50: #fdf1dd !default; +$orange-100: #f5d9a8 !default; +$orange-200: #e9be74 !default; +$orange-300: #d99530 !default; +$orange-400: #c17d10 !default; +$orange-500: #ab6100 !default; +$orange-600: #9e5400 !default; +$orange-700: #8f4700 !default; +$orange-800: #703800 !default; +$orange-900: #5c2900 !default; +$orange-950: #421f00 !default; $red-50: #fcf1ef !default; $red-100: #fdd4cd !default; @@ -161,6 +152,18 @@ $red-800: #8d1300 !default; $red-900: #660e00 !default; $red-950: #4d0a00 !default; +$purple-50: #f4f0ff !default; +$purple-100: #e1d8f9 !default; +$purple-200: #cbbbf2 !default; +$purple-300: #ac93e6 !default; +$purple-400: #9475db !default; +$purple-500: #7b58cf !default; +$purple-600: #694cc0 !default; +$purple-700: #5943b6 !default; +$purple-800: #453894 !default; +$purple-900: #2f2a6b !default; +$purple-950: #232150 !default; + $gray-10: #fafafa !default; $gray-50: #f0f0f0 !default; $gray-100: #dbdbdb !default; @@ -230,6 +233,20 @@ $reds: ( '950': $red-950 ); +$purples: ( + '50': $purple-50, + '100': $purple-100, + '200': $purple-200, + '300': $purple-300, + '400': $purple-400, + '500': $purple-500, + '600': $purple-600, + '700': $purple-700, + '800': $purple-800, + '900': $purple-900, + '950': $purple-950 +); + $grays: ( '10': $gray-10, '50': $gray-50, @@ -357,13 +374,10 @@ $gl-text-color-inverted: $white; $gl-text-color-secondary-inverted: rgba($white, 0.85); $gl-text-color-disabled: $gray-400; $gl-grayish-blue: #7f8fa4; -$gl-gray-dark: #313236; -$gl-gray-light: #5c5c5c; $gl-header-color: #4c4e54; $gl-font-size-12: 12px; $gl-font-size-14: 14px; $gl-font-size-16: 16px; -$gl-font-size-20: 20px; $gl-font-size-28: 28px; $gl-font-size-42: 42px; @@ -484,6 +498,22 @@ $line-added: #ecfdf0; $line-added-dark: #c7f0d2 !default; $line-removed: #fbe9eb; $line-removed-dark: #fac5cd !default; +/* + * The transparent colors are used in Monaco editor. Using full opacity colors + * would hide other layers (selected text, matching brackets). + * + * When the transparent colors get layered on white background, they create their + * full opacity counterparts (computed with https://stackoverflow.com/a/12228643/606571): + * + * - white + $line-added-transparent = $line-added + * - white + $line-added-transparent + $line-added-dark-transparent = $line-added-dark + * + * More details: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41553 + */ +$line-added-transparent: rgba(160, 245, 180, 0.2); +$line-added-dark-transparent: rgba(51, 188, 90, 0.2); +$line-removed-transparent: rgba(235, 145, 155, 0.2); +$line-removed-dark-transparent: rgba(246, 53, 85, 0.2); $line-number-old: #f9d7dc; $line-number-new: #ddfbe6; $line-number-select: #fbf2da; @@ -588,7 +618,6 @@ $award-emoji-width-xs: 90%; $search-input-border-color: rgba($blue-400, 0.8); $search-input-width: 200px; $search-input-xl-width: 320px; -$location-icon-color: #e7e9ed; /* * Notes @@ -894,8 +923,3 @@ $compare-branches-sticky-header-height: 68px; - Issue: https://gitlab.com/gitlab-org/design.gitlab.com/issues/242 */ $enable-validation-icons: false; - -/* -Licenses -*/ -$license-header-cell-width: 150px; diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss index 3eff1807403..55996a074c6 100644 --- a/app/assets/stylesheets/framework/wells.scss +++ b/app/assets/stylesheets/framework/wells.scss @@ -110,10 +110,6 @@ .dark-well { background-color: $gray-normal; - - .btn { - width: 100%; - } } .card.card-body-centered { diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index dd5b99be57e..62abf4a7683 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -35,7 +35,7 @@ .zen-control { padding: 0; - color: $gl-gray-700; + color: $gray-700; background: none; border: 0; } diff --git a/app/assets/stylesheets/mailer.scss b/app/assets/stylesheets/mailer.scss index f188b29a113..a5fc92237df 100644 --- a/app/assets/stylesheets/mailer.scss +++ b/app/assets/stylesheets/mailer.scss @@ -48,6 +48,22 @@ a { font-weight: 500; } +.invite-header { + margin-top: 0; +} + +.invite-actions { + margin-top: 24px; +} + +.invite-btn-join { + border-radius: $border-radius-default; + padding: $gl-btn-vert-padding $gl-btn-horz-padding; + cursor: pointer; + background-color: $purple; + color: $white; +} + tr td { font-family: $mailer-font; } diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss index 6320c10fb51..d281f62c370 100644 --- a/app/assets/stylesheets/notify.scss +++ b/app/assets/stylesheets/notify.scss @@ -7,12 +7,12 @@ img { p.details { font-style: italic; - color: $gl-gray-500; + color: $gray-500; } .footer > p { font-size: small; - color: $gl-gray-500; + color: $gray-500; } pre.commit-message { diff --git a/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss index 0b847902525..57053c7f0cb 100644 --- a/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss +++ b/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss @@ -19,14 +19,6 @@ display: none; } - .monaco-editor .selected-text { - z-index: 1; - } - - .monaco-editor .view-lines { - z-index: 2; - } - .is-readonly .editor.original { .view-lines { cursor: default; @@ -98,11 +90,11 @@ } .char-insert { - background-color: $line-added-dark; + background-color: $line-added-dark-transparent; } .char-delete { - background-color: $line-removed-dark; + background-color: $line-removed-dark-transparent; } .line-numbers { @@ -111,11 +103,11 @@ .view-overlays { .line-insert { - background-color: $line-added; + background-color: $line-added-transparent; } .line-delete { - background-color: $line-removed; + background-color: $line-removed-transparent; } } diff --git a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss index dfd7fd355a4..1e239877428 100644 --- a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss +++ b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss @@ -84,7 +84,8 @@ color: var(--ide-input-border, $gl-text-color-tertiary); } - .dropdown-input .fa { + .dropdown-input .fa, + .dropdown-input .dropdown-input-clear { color: var(--ide-input-border, $dropdown-input-fa-color); } diff --git a/app/assets/stylesheets/page_bundles/_mixins_and_variables_and_functions.scss b/app/assets/stylesheets/page_bundles/_mixins_and_variables_and_functions.scss new file mode 100644 index 00000000000..0e8ea5e2d52 --- /dev/null +++ b/app/assets/stylesheets/page_bundles/_mixins_and_variables_and_functions.scss @@ -0,0 +1,21 @@ +/** + This file contains only imports of Bootstrap, GitLab UI and GitLab mixins, + variables and functions, in the correct order. + + It is meant to be used in page_bundles, but SHOULD NOT introduce any + styles of it's own. We actually check in CI that compiling _this_ file doesn't + result in any additional styles. + + See: scripts/frontend/check_page_bundle_mixins_css_for_sideeffects.js + */ +@import 'framework/variables'; +@import 'framework/variables_overrides'; +@import 'framework/mixins'; + +@import 'bootstrap/scss/functions'; +@import 'bootstrap/scss/variables'; +@import 'bootstrap/scss/mixins'; + +@import '@gitlab/ui/src/scss/functions'; +@import '@gitlab/ui/src/scss/variables'; +@import '@gitlab/ui/src/scss/utility-mixins/index'; diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 36587ecde3d..71e74297ee8 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -1080,7 +1080,7 @@ $ide-commit-header-height: 48px; max-width: 24px; padding: 0; margin: 0 ($grid-size / 2); - color: var(--ide-text-color-secondary, $gl-gray-light); + color: var(--ide-text-color-secondary, $gray-600); &:first-child { margin-left: 0; diff --git a/app/assets/stylesheets/page_bundles/jira_connect.scss b/app/assets/stylesheets/page_bundles/jira_connect.scss new file mode 100644 index 00000000000..83d16f29d49 --- /dev/null +++ b/app/assets/stylesheets/page_bundles/jira_connect.scss @@ -0,0 +1,33 @@ +@import 'framework/variables'; + +$atlaskit-border-color: #dfe1e6; + +.ac-content { + margin: 20px; + + .subscription-form { + margin-bottom: 20px; + + .field-group-input { + display: flex; + padding-top: $gl-padding-4; + + .ak-button { + height: auto; + margin-left: $btn-margin-5; + } + } + } +} + +.subscriptions { + tbody { + tr { + border-bottom: 1px solid $atlaskit-border-color; + } + + td { + padding: $gl-padding-8; + } + } +} diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/page_bundles/todos.scss index c6f104a024b..3eec5b53a30 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/page_bundles/todos.scss @@ -1,3 +1,5 @@ +@import 'mixins_and_variables_and_functions'; + /** * Dashboard Todos * @@ -10,8 +12,8 @@ flex-direction: row; &:hover { - background-color: $blue-50; - border-color: $blue-200; + background-color: var(--blue-50, $blue-50); + border-color: var(--blue-200, $blue-200); cursor: pointer; } @@ -20,7 +22,7 @@ border-bottom: 1px solid transparent; &:hover { - border-color: $blue-200; + border-color: var(--blue-200, $blue-200); } } @@ -44,11 +46,9 @@ } &.todo-pending.done-reversible { - background-color: $white; - &:hover { - border-color: $white-normal; - background-color: $gray-light; + border-color: var(--border-color, $border-color); + background-color: var(--gray-50, $gray-50); border-top: 1px solid transparent; .todo-avatar, @@ -63,7 +63,7 @@ } .btn { - background-color: $gray-light; + background-color: var(--gray-50, $gray-50); } } } @@ -103,15 +103,15 @@ .todo-label, .todo-project { a { - color: $blue-600; font-weight: $gl-font-weight-normal; + color: var(--blue-600, $blue-600); } } .todo-body { .badge.badge-pill, p { - color: $gl-text-color; + color: var(--gl-text-color, $gl-text-color); } .md { @@ -125,9 +125,9 @@ pre { border: 0; - background: $gray-light; + background: var(--gray-50, $gray-50); border-radius: 0; - color: $gl-gray-500; + color: var(--gray-500, $gray-500); margin: 0 20px; overflow: hidden; } @@ -149,18 +149,6 @@ } } -@include media-breakpoint-down(sm) { - .todos-filters { - .dropdown-menu-toggle { - width: 130px; - } - - .dropdown-menu-toggle-sort { - width: auto; - } - } -} - @include media-breakpoint-down(lg) { .todos-filters { .filter-categories { @@ -174,6 +162,10 @@ } @include media-breakpoint-down(sm) { + .container-fluid .todos-list-container { + margin: 0 (-$gl-padding); + } + .todo { .avatar { display: none; @@ -191,7 +183,7 @@ .todo-body { margin: 0; - border-left: 2px solid $gl-gray-100; + border-left: 2px solid var(--border-color, $border-color); padding-left: 10px; } } @@ -204,6 +196,10 @@ .dropdown-menu-toggle { width: 100%; } + + .dropdown-menu-toggle-sort { + width: auto; + } } } diff --git a/app/assets/stylesheets/pages/alert_management/details.scss b/app/assets/stylesheets/pages/alert_management/details.scss index 0f889935583..a104c06c853 100644 --- a/app/assets/stylesheets/pages/alert_management/details.scss +++ b/app/assets/stylesheets/pages/alert_management/details.scss @@ -1,39 +1,4 @@ .alert-management-details { - // these styles need to be deleted once GlTable component looks in GitLab same as in @gitlab/ui - table { - tr { - td { - @include gl-border-0; - @include gl-p-5; - border-color: transparent; - border-bottom: 1px solid $table-border-color; - - &:first-child { - div { - font-weight: bold; - } - } - - &:not(:first-child) { - &::before { - color: $gray-500; - font-weight: normal !important; - } - - div { - color: $gray-500; - } - } - - @include media-breakpoint-up(sm) { - div { - text-align: left !important; - } - } - } - } - } - @include media-breakpoint-down(xs) { .alert-details-incident-button { width: 100%; @@ -67,6 +32,10 @@ } } + .main-notes-list::before { + left: 15px !important; + } + .note-header-info { @include gl-mt-1; } diff --git a/app/assets/stylesheets/pages/alert_management/severity-icons.scss b/app/assets/stylesheets/pages/alert_management/severity-icons.scss index 6004697b3e1..f58ad87a673 100644 --- a/app/assets/stylesheets/pages/alert_management/severity-icons.scss +++ b/app/assets/stylesheets/pages/alert_management/severity-icons.scss @@ -1,3 +1,4 @@ +.incident-severity, .incident-management-list, .alert-management-details { .icon-critical { diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 51a65b88cd0..c4852974a4d 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -116,7 +116,6 @@ .board-title { flex-direction: column; - height: 100%; } .board-title-caret { @@ -284,7 +283,7 @@ } .confidential-icon { - color: $orange-600; + color: $orange-500; cursor: help; } diff --git a/app/assets/stylesheets/pages/branches.scss b/app/assets/stylesheets/pages/branches.scss index 3c49cc54ac4..d34d309eea3 100644 --- a/app/assets/stylesheets/pages/branches.scss +++ b/app/assets/stylesheets/pages/branches.scss @@ -23,7 +23,7 @@ .bar { height: 4px; - background-color: $gl-gray-100; + background-color: $gray-100; } .count { @@ -34,7 +34,7 @@ .graph-separator { width: $graph-separator-width; height: 18px; - background-color: $gl-gray-100; + background-color: $gray-100; } } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index f367d9ea4d8..04167cbee1b 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -50,7 +50,7 @@ top: $header-height; border-radius: 2px 2px 0 0; color: $orange-600; - background-color: $orange-100; + background-color: $orange-50; border: 1px solid $border-gray-normal; padding: 3px 12px; margin: auto; @@ -63,7 +63,7 @@ } .top-bar { - @include build-trace-top-bar(35px); + @include build-trace-top-bar(50px); &.has-archived-block { top: $header-height + 28px; diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index 29422c1f7fa..4e27f438e36 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -69,7 +69,7 @@ align-self: flex-start; font-weight: 500; font-size: 20px; - color: $orange-900; + color: $orange-500; opacity: 1; margin: $gl-padding-8 14px 0 0; } @@ -124,7 +124,7 @@ background-color: none; border: 0; font-weight: bold; - color: $gl-gray-500; + color: $gray-500; } } } @@ -156,7 +156,7 @@ } .cluster-deployments-warning { - color: $orange-600; + color: $orange-500; } .badge.pods-badge { diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index b97709e140f..c509bf121bc 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -41,15 +41,6 @@ width: 20%; } - .fa, - svg { - color: $cycle-analytics-light-gray; - - &:hover { - color: $gl-text-color; - } - } - .stage-header { width: 20.5%; } @@ -360,23 +351,3 @@ } } } - -.cycle-analytics-overview { - padding-top: 100px; - - .overview-details { - display: flex; - align-items: center; - } - - .overview-image { - text-align: right; - } - - .overview-icon { - svg { - width: 365px; - height: 227px; - } - } -} diff --git a/app/assets/stylesheets/pages/dev_ops_score.scss b/app/assets/stylesheets/pages/dev_ops_report.scss index b9ee905f4b6..871cd9c4f02 100644 --- a/app/assets/stylesheets/pages/dev_ops_score.scss +++ b/app/assets/stylesheets/pages/dev_ops_report.scss @@ -25,6 +25,10 @@ $space-between-cards: 8px; margin-left: 8px; font-weight: $gl-font-weight-normal; + .devops-header-icon { + vertical-align: px-to-rem(-$gl-spacing-scale-1); + } + a { font-size: 18px; color: $gl-text-color-secondary; @@ -88,7 +92,7 @@ $space-between-cards: 8px; } .devops-card-average { - border-top-color: $orange-400; + border-top-color: $orange-200; .board-card-score-big { background-color: $orange-50; @@ -247,7 +251,7 @@ $space-between-cards: 8px; } .devops-average-score { - color: $orange-400; + color: $orange-500; } .devops-low-score { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index a7b93c9eab7..62af7103b39 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -102,7 +102,7 @@ .file-mode-changed { padding: 10px; - color: $gl-gray-500; + color: $gray-500; } .suppressed-container { @@ -181,7 +181,7 @@ .swipe-wrap { overflow: hidden; - border-right: 1px solid $gl-gray-400; + border-right: 1px solid $gray-300; position: absolute; display: block; top: 13px; @@ -190,7 +190,7 @@ &.left-oriented { /* only for commit view (different swipe viewer) */ border-right: 0; - border-left: 1px solid $gl-gray-400; + border-left: 1px solid $gray-300; } } @@ -1062,7 +1062,7 @@ table.code { .diff-tree-list { position: -webkit-sticky; position: sticky; - $top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 11px; + $top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 15px; top: $top-pos; max-height: calc(100vh - #{$top-pos}); z-index: 202; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index ef7b56ac210..5ce500dad1d 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -83,7 +83,7 @@ .x-axis path, .y-axis path { - stroke: $gl-gray-350; + stroke: $gray-300; } .label-x-axis-line, @@ -93,7 +93,7 @@ .y-axis { line { - stroke: $gl-gray-350; + stroke: $gray-300; stroke-width: 1; } } @@ -117,7 +117,7 @@ } .selected-metric-line { - stroke: $gl-gray-dark; + stroke: $gray-900; stroke-width: 1; } diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 500f5816d38..5738cbb4b31 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -94,7 +94,7 @@ border: 0; background: $gray-light; border-radius: 0; - color: $gl-gray-500; + color: $gray-500; overflow: hidden; } @@ -111,7 +111,7 @@ } .event-note-icon { - color: $gl-gray-500; + color: $gray-500; float: left; font-size: $gl-font-size; margin-right: 5px; diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss index c4b6cdd703d..bca4d50973a 100644 --- a/app/assets/stylesheets/pages/graph.scss +++ b/app/assets/stylesheets/pages/graph.scss @@ -43,7 +43,7 @@ .y-axis-label { line { - stroke: $gl-gray-350; + stroke: $gray-300; } text { diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index c309c8d157a..69fd094f83b 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -52,7 +52,7 @@ .save-group-loader { margin-top: $gl-padding-50; margin-bottom: $gl-padding-50; - color: $gl-gray-700; + color: $gray-700; } .group-nav-container .nav-controls { diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index ab281bc7f23..540060d60de 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -1,6 +1,6 @@ .shortcut-mappings { font-size: 12px; - color: $gl-gray-700; + color: $gray-700; tbody:first-child tr:first-child { padding-top: 0; @@ -22,7 +22,7 @@ .shortcut { padding-right: 10px; - color: $gl-gray-400; + color: $gray-300; text-align: right; white-space: nowrap; } diff --git a/app/assets/stylesheets/pages/incident_management_list.scss b/app/assets/stylesheets/pages/incident_management_list.scss index 00ca3cc73e0..316066694a8 100644 --- a/app/assets/stylesheets/pages/incident_management_list.scss +++ b/app/assets/stylesheets/pages/incident_management_list.scss @@ -108,7 +108,7 @@ border-bottom-width: 0; .gl-tab-nav-item { - color: $gl-gray-600; + color: $gray-500; > .gl-tab-counter-badge { color: inherit; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 2f28361f62c..53525a4d877 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -1,5 +1,5 @@ .issuable-warning-icon { - background-color: $orange-100; + background-color: $orange-50; border-radius: $border-radius-default; width: $issuable-warning-size; height: $issuable-warning-size; @@ -50,6 +50,7 @@ .title-container { display: flex; + align-items: flex-start; } .title { @@ -65,7 +66,6 @@ .btn-edit { margin-left: auto; - height: $gl-padding * 2; } .emoji-block { @@ -119,11 +119,11 @@ .assignee { .merge-icon { - color: $orange-500; + color: $orange-400; position: absolute; bottom: 0; right: 0; - text-shadow: -1px -1px 0 $white, 1px -1px 0 $white, -1px 1px 0 $white, 1px 1px 0 $white; + text-shadow: -1px -1px 2px $white, 1px -1px 2px $white, -1px 1px 2px $white, 1px 1px 2px $white; } } @@ -234,8 +234,8 @@ .title { color: $gl-text-color; - margin-bottom: $gl-padding-8; - line-height: 1; + margin-bottom: $gl-padding-4; + line-height: $gl-line-height-20; .avatar { margin-left: 0; @@ -252,7 +252,8 @@ } } - .cross-project-reference { + .cross-project-reference, + .sidebar-mr-source-branch { color: inherit; span { @@ -360,13 +361,6 @@ margin: 0; } - .username { - display: block; - margin-top: 4px; - font-size: 13px; - font-weight: $gl-font-weight-normal; - } - .hide-expanded { display: none; } @@ -389,7 +383,8 @@ border-bottom: 0; overflow: hidden; - &:hover { + &.with-sub-blocks .sub-block:hover, + &:not(.with-sub-blocks):hover { background-color: $gray-100; } @@ -444,11 +439,6 @@ } } - span { - display: block; - margin-top: 0; - } - .sidebar-avatar-counter { padding-top: 2px; } @@ -741,7 +731,6 @@ .issuable-info-container { flex: 1; display: flex; - padding-right: $gl-padding; .issuable-main-info { flex: 1 auto; @@ -919,12 +908,12 @@ } .issuable-todo-btn { - .fa-spinner { + .gl-spinner { display: none; } &.is-loading { - .fa-spinner { + .gl-spinner { display: inline-block; } @@ -982,7 +971,7 @@ } .suggestion-confidential { - color: $orange-600; + color: $orange-500; } .suggestion-state-open { @@ -1006,3 +995,15 @@ border: 0; } } + +@include media-breakpoint-down(sm) { + // Overriding the following rule with the negative margin + // https://gitlab.com/gitlab-org/gitlab/-/blob/146c43c931c3743a140529307aea616e4aa9ff21/app/assets/stylesheets/framework/sidebar.scss#L1-5 + .container-fluid { + .issuable-list, + .issues-filters, + .epics-filters { + margin: 0 (-$gl-padding); + } + } +} diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 0c349ab73a3..03603f637c8 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -14,7 +14,7 @@ } .issue { - padding: 10px 0 10px $gl-padding; + padding: 10px $gl-padding; position: relative; .title { @@ -294,12 +294,12 @@ ul.related-merge-requests > li { &::after { content: image-url('icon_anchor.svg'); - @include invisible(hidden); + visibility: hidden; } } &:hover > a.anchor::after { - @include invisible(visible); + visibility: visible; } } } diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index 54bca80194f..2d9a9f3029f 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -180,10 +180,6 @@ word-break: break-all; } - .member-group-link { - display: inline-block; - } - .form-control { width: inherit; } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index a6d1fc11c3f..8aaeb92eb7a 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -64,7 +64,7 @@ $mr-widget-min-height: 69px; background-color: $gray-light; &.clickable:hover { - background-color: $gl-gray-100; + background-color: $gray-100; cursor: pointer; } } @@ -311,7 +311,7 @@ $mr-widget-min-height: 69px; .bold { font-weight: $gl-font-weight-bold; - color: $gl-gray-light; + color: $gray-600; margin-left: 10px; } @@ -873,7 +873,6 @@ $mr-widget-min-height: 69px; .merge-request-tabs-container, .epic-tabs-container { flex-direction: column-reverse; - padding-top: $gl-padding-8; } } @@ -883,7 +882,6 @@ $mr-widget-min-height: 69px; .epic-tabs-container { flex-direction: column-reverse; align-items: flex-start; - padding-top: $gl-padding-8; } } } @@ -980,7 +978,7 @@ $mr-widget-min-height: 69px; opacity: 0.65; &:hover { - color: $gl-gray-500; + color: $gray-500; text-decoration: none; } } @@ -1035,3 +1033,9 @@ $mr-widget-min-height: 69px; .diff-file-row.is-active { background-color: $gray-50; } + +.merge-request-container { + .flash-container { + @include gl-mb-4; + } +} diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index c473cc44637..e9eb79b071c 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -178,7 +178,6 @@ $status-box-line-height: 26px; .milestone-detail { border-bottom: 1px solid $border-color; - padding: 20px 0; } @include media-breakpoint-down(xs) { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 35a15214f68..8b3d3268a8c 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -97,7 +97,7 @@ .issuable-note-warning { color: $orange-600; - background-color: $orange-100; + background-color: $orange-50; border-radius: $border-radius-default $border-radius-default 0 0; border: 1px solid $border-gray-normal; border-bottom: 0; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index e4e54501627..c144fb13322 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -186,8 +186,8 @@ $note-form-margin-left: 72px; padding: $gl-padding; .dummy-avatar { - background-color: $gl-gray-100; - border: 1px solid darken($gl-gray-100, 25%); + background-color: $gray-100; + border: 1px solid darken($gray-100, 25%); } .note-headline-light, @@ -835,7 +835,7 @@ $note-form-margin-left: 72px; &[disabled] { background: $white; border-color: $gray-200; - color: $gl-gray-400; + color: $gray-300; cursor: not-allowed; } } diff --git a/app/assets/stylesheets/pages/packages.scss b/app/assets/stylesheets/pages/packages.scss deleted file mode 100644 index 8f6eee524e5..00000000000 --- a/app/assets/stylesheets/pages/packages.scss +++ /dev/null @@ -1,11 +0,0 @@ -.commit-row-description { - border: 0; - border-left: 3px solid $white-dark; -} - -.package-list-table[aria-busy='true'] { - td { - padding-bottom: 0; - padding-top: 0; - } -} diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index fc3b786b365..8b104ce9017 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -193,7 +193,6 @@ .icon-container { display: inline-block; - width: 10px; &.commit-icon { width: 15px; @@ -782,7 +781,7 @@ &.ci-status-icon-pending, &.ci-status-icon-waiting-for-resource, &.ci-status-icon-success-with-warnings { - @include mini-pipeline-graph-color($white, $orange-100, $orange-200, $orange-500, $orange-600, $orange-700); + @include mini-pipeline-graph-color($white, $orange-50, $orange-100, $orange-500, $orange-600, $orange-700); } &.ci-status-icon-preparing, @@ -1082,3 +1081,19 @@ button.mini-pipeline-graph-dropdown-toggle { .progress-bar.bg-primary { background-color: $blue-500 !important; } + +.pipeline-stage-pill { + width: 10rem; +} + +.pipeline-job-pill { + width: 8rem; +} + +.stage-left-rounded { + border-radius: 2rem 0 0 2rem; +} + +.stage-right-rounded { + border-radius: 0 2rem 2rem 0; +} diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 85836962b06..4dc1f2034f3 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -257,7 +257,8 @@ } } -table.u2f-registrations { +table.u2f-registrations, +.webauthn-registrations { th:not(:last-child), td:not(:last-child) { border-right: solid 1px transparent; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index d4d6583312c..a2f8447c0b6 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -353,7 +353,7 @@ .save-project-loader { margin-top: 50px; margin-bottom: 50px; - color: $gl-gray-700; + color: $gray-700; } .transfer-project .select2-container { @@ -429,7 +429,7 @@ > li + li::before { padding: 0 3px; - color: $gl-gray-400; + color: $gray-300; } a { @@ -1268,18 +1268,10 @@ pre.light-well { position: relative; .clear-icon { - @extend .fa-times; display: none; position: absolute; - right: 7px; - top: 7px; - color: $location-icon-color; - - &::before { - font-family: FontAwesome; - font-weight: $gl-font-weight-normal; - font-style: normal; - } + right: 9px; + top: 9px; } &.has-value { @@ -1444,7 +1436,7 @@ pre.light-well { .project-filters { .btn svg { - color: $gl-gray-700; + color: $gray-700; } .button-filter-group { diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 1fc6ad62237..4b8e1da4867 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -105,7 +105,7 @@ input[type='checkbox']:hover { } .dropdown-header { - // Necessary because glDropdown doesn't support a second style of headers + // Necessary because deprecatedJQueryDropdown doesn't support a second style of headers font-weight: $gl-font-weight-bold; color: $gl-text-color; font-size: $gl-font-size; diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index b82c638a5b7..7b18e3774d8 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -124,8 +124,8 @@ .settings-message { padding: 5px; line-height: 1.3; - color: $orange-700; - background-color: $orange-100; + color: $gray-900; + background-color: $orange-50; border: 1px solid $orange-200; border-radius: $border-radius-base; } @@ -135,7 +135,7 @@ } .warning-title { - color: $orange-500; + color: $gray-900; } .danger-title { @@ -301,7 +301,7 @@ } .loading-metrics .metrics-load-spinner { - color: $gl-gray-700; + color: $gray-700; } .metrics-list { @@ -384,6 +384,13 @@ font-weight: $gl-font-weight-bold; border: 0; } + + // When tables are "stacked", restore td padding + @media(max-width: map-get($grid-breakpoints, lg)) { + td { + padding-left: $gl-spacing-scale-5; + } + } } } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 4ad2dcbe92f..b37c5172ad2 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -43,7 +43,7 @@ &.ci-waiting-for-resource, &.ci-failed-with-warnings, &.ci-success-with-warnings { - @include status-color($orange-100, $orange-500, $orange-700); + @include status-color($orange-50, $orange-500, $orange-700); } &.ci-info, diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index b6af395a802..73fe76f139f 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -173,7 +173,7 @@ .tree-truncated-warning { color: $orange-600; - background-color: $orange-100; + background-color: $orange-50; } .tree-time-ago { diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss index 7744fd814d0..288da4da5c3 100644 --- a/app/assets/stylesheets/pages/ui_dev_kit.scss +++ b/app/assets/stylesheets/pages/ui_dev_kit.scss @@ -6,7 +6,7 @@ .example { padding: 15px; - border: 1px dashed $gl-gray-100; + border: 1px dashed $gray-100; margin-bottom: 15px; &::before { diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index 8c4bfdf68cc..ccf11058b5b 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -151,3 +151,7 @@ ul.wiki-pages-list.content-list { .empty-state-wiki .text-content { max-width: 490px; // Widen to allow for the Confluence button } + +.wiki-form .markdown-area { + max-height: none; +} diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss index daeab80d373..dc127cd2554 100644 --- a/app/assets/stylesheets/performance_bar.scss +++ b/app/assets/stylesheets/performance_bar.scss @@ -11,15 +11,15 @@ height: $performance-bar-height; background: $black; line-height: $performance-bar-height; - color: $gl-gray-400; + color: $gray-300; select { - color: $gl-gray-400; + color: $gray-300; width: 200px; } input { - color: $gl-gray-400; + color: $gray-300; width: $input-short-width - 60px; } @@ -61,7 +61,7 @@ padding: 4px 6px; font-family: Consolas, 'Liberation Mono', Courier, monospace; line-height: 1; - color: $gl-gray-100; + color: $gray-100; border-radius: 3px; box-shadow: 0 1px 0 $perf-bar-bucket-box-shadow-from, inset 0 1px 2px $perf-bar-bucket-box-shadow-to; diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss new file mode 100644 index 00000000000..d875f758ead --- /dev/null +++ b/app/assets/stylesheets/startup/startup-dark.scss @@ -0,0 +1,1912 @@ +@charset "UTF-8"; +*, +*::before, +*::after { + box-sizing: border-box; +} +html { + font-family: sans-serif; + line-height: 1.15; +} + header, nav, section { + display: block; +} +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #fafafa; + text-align: left; + background-color: #2e2e2e; +} +h1, h2, h3 { + margin-top: 0; + margin-bottom: 0.25rem; +} +p { + margin-top: 0; + margin-bottom: 1rem; +} + +ul { + margin-top: 0; + margin-bottom: 1rem; +} + +ul ul { + margin-bottom: 0; +} + +strong { + font-weight: bolder; +} +sub { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} +sub { + bottom: -.25em; +} +a { + color: #007bff; + text-decoration: none; + background-color: transparent; +} +a:not([href]) { + color: inherit; + text-decoration: none; +} +pre, +code { + font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace; + font-size: 1em; +} +pre { + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; +} +img { + vertical-align: middle; + border-style: none; +} +svg { + overflow: hidden; + vertical-align: middle; +} +table { + border-collapse: collapse; +} +th { + text-align: inherit; +} +button { + border-radius: 0; +} +input, +button, +textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} +button, +input { + overflow: visible; +} +button { + text-transform: none; +} +button:not(:disabled), +[type="button"]:not(:disabled), +[type="reset"]:not(:disabled) { + cursor: pointer; +} +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner { + padding: 0; + border-style: none; +} +textarea { + overflow: auto; + resize: vertical; +} +[type="search"] { + outline-offset: -2px; +} +summary { + display: list-item; + cursor: pointer; +} +template { + display: none; +} +[hidden] { + display: none !important; +} +h1, h2, h3, +.h1, .h2, .h3 { + margin-bottom: 0.25rem; + font-weight: 600; + line-height: 1.2; + color: #fafafa; +} +h1, .h1 { + font-size: 2.1875rem; +} +h2, .h2 { + font-size: 1.75rem; +} +h3, .h3 { + font-size: 1.53125rem; +} +.list-unstyled { + padding-left: 0; + list-style: none; +} +code { + font-size: 90%; + color: #fff; + word-wrap: break-word; +} +a > code { + color: inherit; +} +pre { + display: block; + font-size: 90%; + color: #fafafa; +} +pre code { + font-size: inherit; + color: inherit; + word-break: normal; +} +.container { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container { + max-width: 1140px; + } +} +.container-fluid { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container { + max-width: 1140px; + } +} +.row { + display: flex; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; +} +.table { + width: 100%; + margin-bottom: 0.5rem; + color: #fafafa; +} +.table th, +.table td { + padding: 0.75rem; + vertical-align: top; + border-top: 1px solid #4f4f4f; +} + .search form { + display: block; + width: 100%; + height: 34px; + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + font-weight: 400; + line-height: 1.5; + color: #fafafa; + background-color: #4f4f4f; + background-clip: padding-box; + border: 1px solid #4f4f4f; + border-radius: 0.25rem; +} + +@media (prefers-reduced-motion: reduce) { +} + .search form:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 #fafafa; +} + .search form::placeholder { + color: #ccc; + opacity: 1; +} + .search form:disabled { + background-color: #2e2e2e; + opacity: 1; +} +.form-inline { + display: flex; + flex-flow: row wrap; + align-items: center; +} + +@media (min-width: 576px) { + .form-inline .search form, .search .form-inline form { + display: inline-block; + width: auto; + vertical-align: middle; + } +} +.btn { + display: inline-block; + font-weight: 400; + color: #fafafa; + text-align: center; + vertical-align: middle; + cursor: pointer; + user-select: none; + background-color: transparent; + border: 1px solid transparent; + padding: 0.375rem 0.75rem; + font-size: 1rem; + line-height: 20px; + border-radius: 0.25rem; +} + +@media (prefers-reduced-motion: reduce) { +} +.btn.disabled, .btn:disabled { + opacity: 0.65; +} +a.btn.disabled { + pointer-events: none; +} +.collapse:not(.show) { + display: none; +} + +.dropdown { + position: relative; +} + .dropdown-menu-toggle { + white-space: nowrap; +} + .dropdown-menu-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid; + border-right: 0.3em solid transparent; + border-bottom: 0; + border-left: 0.3em solid transparent; +} + .dropdown-menu-toggle:empty::after { + margin-left: 0; +} +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 10rem; + padding: 0.5rem 0; + margin: 0.125rem 0 0; + font-size: 1rem; + color: #fafafa; + text-align: left; + list-style: none; + background-color: #333; + background-clip: padding-box; + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 0.25rem; +} +.dropdown-menu-right { + right: 0; + left: auto; +} + .divider { + height: 0; + margin: 4px 0; + overflow: hidden; + border-top: 1px solid #4f4f4f; +} +.dropdown-menu.show { + display: block; +} +.nav { + display: flex; + flex-wrap: wrap; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} +.navbar { + position: relative; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + padding: 0.25rem 0.5rem; +} +.navbar .container, +.navbar .container-fluid { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; +} +.navbar-nav { + display: flex; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} +.navbar-nav .dropdown-menu { + position: static; + float: none; +} +.navbar-collapse { + flex-basis: 100%; + flex-grow: 1; + align-items: center; +} +.navbar-toggler { + padding: 0.25rem 0.75rem; + font-size: 1.25rem; + line-height: 1; + background-color: transparent; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +@media (max-width: 575.98px) { + .navbar-expand-sm > .container, + .navbar-expand-sm > .container-fluid { + padding-right: 0; + padding-left: 0; + } +} + +@media (min-width: 576px) { + .navbar-expand-sm { + flex-flow: row nowrap; + justify-content: flex-start; + } + .navbar-expand-sm .navbar-nav { + flex-direction: row; + } + .navbar-expand-sm .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-sm > .container, + .navbar-expand-sm > .container-fluid { + flex-wrap: nowrap; + } + .navbar-expand-sm .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-sm .navbar-toggler { + display: none; + } +} +.card { + position: relative; + display: flex; + flex-direction: column; + min-width: 0; + word-wrap: break-word; + background-color: #333; + background-clip: border-box; + border: 1px solid #4f4f4f; + border-radius: 0.25rem; +} +.badge { + display: inline-block; + padding: 0.25em 0.4em; + font-size: 75%; + font-weight: 600; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25rem; +} + +@media (prefers-reduced-motion: reduce) { +} +.badge:empty { + display: none; +} +.btn .badge { + position: relative; + top: -1px; +} +.badge-pill { + padding-right: 0.6em; + padding-left: 0.6em; + border-radius: 10rem; +} +.media { + display: flex; + align-items: flex-start; +} +.close { + float: right; + font-size: 1.5rem; + font-weight: 600; + line-height: 1; + color: #fff; + text-shadow: 0 1px 0 #333; + opacity: .5; +} +button.close { + padding: 0; + background-color: transparent; + border: 0; + appearance: none; +} +a.close.disabled { + pointer-events: none; +} +.modal-dialog { + position: relative; + width: auto; + margin: 0.5rem; + pointer-events: none; +} + +@media (min-width: 576px) { + .modal-dialog { + max-width: 500px; + margin: 1.75rem auto; + } +} +.bg-transparent { + background-color: transparent !important; +} +.border { + border: 1px solid #4f4f4f !important; +} +.border-top { + border-top: 1px solid #4f4f4f !important; +} +.border-right { + border-right: 1px solid #4f4f4f !important; +} +.border-bottom { + border-bottom: 1px solid #4f4f4f !important; +} +.border-left { + border-left: 1px solid #4f4f4f !important; +} +.rounded { + border-radius: 0.25rem !important; +} +.clearfix::after { + display: block; + clear: both; + content: ""; +} +.d-none { + display: none !important; +} +.d-inline-block { + display: inline-block !important; +} +.d-block { + display: block !important; +} + +@media (min-width: 576px) { + .d-sm-none { + display: none !important; + } +} + +@media (min-width: 768px) { + .d-md-block { + display: block !important; + } +} + +@media (min-width: 992px) { + .d-lg-none { + display: none !important; + } + .d-lg-block { + display: block !important; + } +} + +@media (min-width: 1200px) { + .d-xl-block { + display: block !important; + } +} +.flex-wrap { + flex-wrap: wrap !important; +} +.float-right { + float: right !important; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} +.m-auto { + margin: auto !important; +} +.text-nowrap { + white-space: nowrap !important; +} +.visible { + visibility: visible !important; +} + .search form.focus { + color: #fafafa; + background-color: #4f4f4f; + border-color: #80bdff; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} +.gl-badge { + display: inline-flex; + align-items: center; + font-size: 0.75rem; + font-weight: 400; + line-height: 1rem; + padding-top: 0.25rem; + padding-bottom: 0.25rem; + padding-left: 0.5rem; + padding-right: 0.5rem; + outline: none; +} +body, .search form, +.search form { + font-size: 0.875rem; +} +button, +html [type='button'], +[type='reset'], +[role='button'] { + cursor: pointer; +} +h1, +.h1, +h2, +.h2, +h3, +.h3 { + margin-top: 20px; + margin-bottom: 10px; +} +input[type='file'] { + line-height: 1; +} + +strong { + font-weight: bold; +} +a { + color: #418cd8; +} +code { + padding: 2px 4px; + color: #fff; + background-color: #2e2e2e; + border-radius: 4px; +} +.code > code { + background-color: inherit; + padding: unset; +} +table { + border-spacing: 0; +} +.hidden { + display: none !important; + visibility: hidden !important; +} +.hide { + display: none; +} + .dropdown-menu-toggle::after { + display: none; +} +.badge:not(.gl-badge) { + padding: 4px 5px; + font-size: 12px; + font-style: normal; + font-weight: 400; + display: inline-block; +} +pre code { + white-space: pre-wrap; +} +.toggle-sidebar-button .collapse-text, +.toggle-sidebar-button .icon-chevron-double-lg-left, +.toggle-sidebar-button .icon-chevron-double-lg-right { + color: #bababa; +} +svg { + vertical-align: baseline; +} +html { + overflow-y: scroll; +} +body { + text-decoration-skip: ink; +} +.content-wrapper { + margin-top: 40px; + padding-bottom: 100px; +} +.container { + padding-top: 0; + z-index: 5; +} +.container .content { + margin: 0; +} + +@media (max-width: 575.98px) { + .container .content { + margin-top: 20px; + } +} + +@media (max-width: 575.98px) { + .container .container .title { + padding-left: 15px !important; + } +} +.btn { + border-radius: 4px; + font-size: 0.875rem; + font-weight: 400; + padding: 6px 10px; + background-color: #333; + border-color: #4f4f4f; + color: #fafafa; + color: #fafafa; + white-space: nowrap; +} +.btn:active, .btn.active { + box-shadow: rgba(0, 0, 0, 0.16); + background-color: #444; + border-color: #fafafa; + color: #fafafa; +} +.btn svg { + height: 15px; + width: 15px; +} +.btn svg:not(:last-child), +.btn .fa:not(:last-child) { + margin-right: 5px; +} +.badge.badge-pill:not(.gl-badge) { + font-weight: 400; + background-color: rgba(0, 0, 0, 0.07); + color: #dfdfdf; + vertical-align: baseline; +} +.hint { + font-style: italic; + color: #707070; +} +.bold { + font-weight: 600; +} +pre.wrap { + word-break: break-word; + white-space: pre-wrap; +} +table a code { + position: relative; + top: -2px; + margin-right: 3px; +} +.loading { + margin: 20px auto; + height: 40px; + color: #dfdfdf; + font-size: 32px; + text-align: center; +} +.highlight { + text-shadow: none; +} +.chart { + overflow: hidden; + height: 220px; +} +.break-word { + word-wrap: break-word; +} +.center { + text-align: center; +} +.block { + display: block; +} +.flex { + display: flex; +} +.flex-grow { + flex-grow: 1; +} +.dropdown { + position: relative; +} +.show.dropdown .dropdown-menu { + transform: translateY(0); + display: block; + min-height: 40px; + max-height: 312px; + overflow-y: auto; +} + +@media (max-width: 575.98px) { + .show.dropdown .dropdown-menu { + width: 100%; + } +} + .show.dropdown .dropdown-menu-toggle, +.show.dropdown .dropdown-menu-toggle { + border-color: #c4c4c4; +} +.show.dropdown [data-toggle='dropdown'] { + outline: 0; +} +.search-input-container .dropdown-menu { + margin-top: 11px; +} + .dropdown-menu-toggle { + padding: 6px 8px 6px 10px; + background-color: #333; + color: #fafafa; + font-size: 14px; + text-align: left; + border: 1px solid #4f4f4f; + border-radius: 0.25rem; + white-space: nowrap; +} + .no-outline.dropdown-menu-toggle { + outline: 0; +} + .dropdown-menu-toggle .fa { + color: #c4c4c4; +} +.dropdown-menu-toggle { + padding-right: 25px; + position: relative; + width: 160px; + text-overflow: ellipsis; + overflow: hidden; +} +.dropdown-menu-toggle .fa { + position: absolute; +} +.dropdown-menu { + display: none; + position: absolute; + width: auto; + top: 100%; + z-index: 300; + min-width: 240px; + max-width: 500px; + margin-top: 4px; + margin-bottom: 24px; + font-size: 14px; + font-weight: 400; + padding: 8px 0; + background-color: #333; + border: 1px solid #4f4f4f; + border-radius: 0.25rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} +.dropdown-menu ul { + margin: 0; + padding: 0; +} +.dropdown-menu li { + display: block; + text-align: left; + list-style: none; + padding: 0 1px; +} +.dropdown-menu li > a, +.dropdown-menu li button { + background: transparent; + border: 0; + border-radius: 0; + box-shadow: none; + display: block; + font-weight: 400; + position: relative; + padding: 8px 12px; + color: #fafafa; + line-height: 16px; + white-space: normal; + overflow: hidden; + text-align: left; + width: 100%; +} +.dropdown-menu .divider { + height: 1px; + margin: 0.25rem 0; + padding: 0; + background-color: #4f4f4f; +} +.dropdown-menu .badge.badge-pill + span:not(.badge.badge-pill) { + margin-right: 40px; +} +.dropdown-select { + width: 300px; +} + +@media (max-width: 767.98px) { + .dropdown-select { + width: 100%; + } +} +.dropdown-content { + max-height: 252px; + overflow-y: auto; +} +.dropdown-loading { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + display: none; + z-index: 9; + background-color: rgba(51, 51, 51, 0.6); + font-size: 28px; +} +.dropdown-loading .fa { + position: absolute; + top: 50%; + left: 50%; + margin-top: -14px; + margin-left: -14px; +} + +@media (max-width: 575.98px) { + .navbar-gitlab li.dropdown { + position: static; + } + header.navbar-gitlab .dropdown .dropdown-menu { + width: 100%; + min-width: 100%; + } +} + +@media (max-width: 767.98px) { + .dropdown-menu-toggle { + width: 100%; + } +} +textarea { + resize: vertical; +} +input { + border-radius: 0.25rem; + color: #fafafa; + background-color: #4f4f4f; +} + .search form { + border-radius: 4px; + padding: 6px 10px; +} + .search form::placeholder { + color: #a7a7a7; +} +body.ui-indigo .navbar-gitlab { + background-color: #292961; +} +body.ui-indigo .navbar-gitlab .navbar-collapse { + color: #d1d1f0; +} +body.ui-indigo .navbar-gitlab .container-fluid .navbar-toggler { + border-left: 1px solid #6868b9; +} +body.ui-indigo .navbar-gitlab .container-fluid .navbar-toggler svg { + fill: #d1d1f0; +} +body.ui-indigo .navbar-gitlab .navbar-sub-nav > li.active > a, +body.ui-indigo .navbar-gitlab .navbar-sub-nav > li.active > button, body.ui-indigo .navbar-gitlab .navbar-sub-nav > li.dropdown.show > a, +body.ui-indigo .navbar-gitlab .navbar-sub-nav > li.dropdown.show > button, +body.ui-indigo .navbar-gitlab .navbar-nav > li.active > a, +body.ui-indigo .navbar-gitlab .navbar-nav > li.active > button, +body.ui-indigo .navbar-gitlab .navbar-nav > li.dropdown.show > a, +body.ui-indigo .navbar-gitlab .navbar-nav > li.dropdown.show > button { + color: #292961; + background-color: #333; +} +body.ui-indigo .navbar-gitlab .navbar-sub-nav { + color: #d1d1f0; +} +body.ui-indigo .navbar-gitlab .nav > li { + color: #d1d1f0; +} +body.ui-indigo .navbar-gitlab .nav > li > a.header-user-dropdown-toggle .header-user-avatar { + border-color: #d1d1f0; +} +body.ui-indigo .navbar-gitlab .nav > li.active > a, +body.ui-indigo .navbar-gitlab .nav > li.dropdown.show > a { + color: #292961; + background-color: #333; +} +body.ui-indigo .search form { + background-color: rgba(209, 209, 240, 0.2); +} +body.ui-indigo .search .search-input::placeholder { + color: rgba(209, 209, 240, 0.8); +} +body.ui-indigo .search .search-input-wrap .search-icon, +body.ui-indigo .search .search-input-wrap .clear-icon { + fill: rgba(209, 209, 240, 0.8); +} +body.ui-indigo .nav-sidebar li.active { + box-shadow: inset 4px 0 0 #4b4ba3; +} +body.ui-indigo .nav-sidebar li.active > a { + color: #393982; +} +body.ui-indigo .nav-sidebar li.active .nav-icon-container svg { + fill: #393982; +} +body.ui-indigo .sidebar-top-level-items > li.active .badge.badge-pill { + color: #393982; +} +body.gl-dark .logo-text svg { + fill: #fafafa; +} +body.gl-dark .navbar-gitlab { + background-color: #2e2e2e; +} +body.gl-dark .navbar-gitlab .navbar-sub-nav li.active > a, +body.gl-dark .navbar-gitlab .navbar-sub-nav li.active > button, +body.gl-dark .navbar-gitlab .navbar-sub-nav li.dropdown.show > a, +body.gl-dark .navbar-gitlab .navbar-sub-nav li.dropdown.show > button, +body.gl-dark .navbar-gitlab .navbar-nav li.active > a, +body.gl-dark .navbar-gitlab .navbar-nav li.active > button, +body.gl-dark .navbar-gitlab .navbar-nav li.dropdown.show > a, +body.gl-dark .navbar-gitlab .navbar-nav li.dropdown.show > button { + color: #fafafa; + background-color: #707070; +} +body.gl-dark .navbar-gitlab .search form { + background-color: #4f4f4f; + box-shadow: inset 0 0 0 1px #4f4f4f; +} +.navbar-gitlab { + padding: 0 16px; + z-index: 1000; + margin-bottom: 0; + min-height: 40px; + border: 0; + border-bottom: 1px solid #4f4f4f; + position: fixed; + top: 0; + left: 0; + right: 0; + border-radius: 0; +} +.navbar-gitlab .logo-text { + line-height: initial; +} +.navbar-gitlab .logo-text svg { + width: 55px; + height: 14px; + margin: 0; + fill: #333; +} +.navbar-gitlab .close-icon { + display: none; +} +.navbar-gitlab .header-content { + width: 100%; + display: flex; + justify-content: space-between; + position: relative; + min-height: 40px; + padding-left: 0; +} +.navbar-gitlab .header-content .title-container { + display: flex; + align-items: stretch; + flex: 1 1 auto; + padding-top: 0; + overflow: visible; +} +.navbar-gitlab .header-content .title { + padding-right: 0; + color: currentColor; + display: flex; + position: relative; + margin: 0; + font-size: 18px; + vertical-align: top; + white-space: nowrap; +} +.navbar-gitlab .header-content .title img { + height: 28px; +} +.navbar-gitlab .header-content .title img + .logo-text { + margin-left: 8px; +} +.navbar-gitlab .header-content .title.wrap { + white-space: normal; +} +.navbar-gitlab .header-content .title a { + display: flex; + align-items: center; + padding: 2px 8px; + margin: 5px 2px 5px -8px; + border-radius: 4px; +} +.navbar-gitlab .header-content .dropdown.open > a { + border-bottom-color: #333; +} +.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) { + margin: 0 2px; +} +.navbar-gitlab .navbar-collapse { + flex: 0 0 auto; + border-top: 0; + padding: 0; +} + +@media (max-width: 575.98px) { + .navbar-gitlab .navbar-collapse { + flex: 1 1 auto; + } +} +.navbar-gitlab .navbar-collapse .nav { + flex-wrap: nowrap; +} + +@media (max-width: 575.98px) { + .navbar-gitlab .navbar-collapse .nav > li:not(.d-none) a { + margin-left: 0; + } +} +.navbar-gitlab .container-fluid { + padding: 0; +} +.navbar-gitlab .container-fluid .user-counter svg { + margin-right: 3px; +} +.navbar-gitlab .container-fluid .navbar-toggler { + position: relative; + right: -10px; + border-radius: 0; + min-width: 45px; + padding: 0; + margin: 8px -7px 8px 0; + font-size: 14px; + text-align: center; + color: currentColor; +} + +@media (max-width: 575.98px) { + .navbar-gitlab .container-fluid .navbar-nav { + display: flex; + padding-right: 10px; + flex-direction: row; + } +} +.navbar-gitlab .container-fluid .navbar-nav li .badge.badge-pill { + box-shadow: none; + font-weight: 600; +} + +@media (max-width: 575.98px) { + .navbar-gitlab .container-fluid .nav > li.header-user { + padding-left: 10px; + } +} +.navbar-gitlab .container-fluid .nav > li > a { + will-change: color; + margin: 4px 0; + padding: 6px 8px; + height: 32px; +} + +@media (max-width: 575.98px) { + .navbar-gitlab .container-fluid .nav > li > a { + padding: 0; + } +} +.navbar-gitlab .container-fluid .nav > li > a.header-user-dropdown-toggle { + margin-left: 2px; +} +.navbar-gitlab .container-fluid .nav > li > a.header-user-dropdown-toggle .header-user-avatar { + margin-right: 0; +} +.navbar-gitlab .container-fluid .nav > li .header-new-dropdown-toggle { + margin-right: 0; +} +.navbar-sub-nav > li > a, +.navbar-sub-nav > li > button, +.navbar-nav > li > a, +.navbar-nav > li > button { + display: flex; + align-items: center; + justify-content: center; + padding: 6px 8px; + margin: 4px 2px; + font-size: 12px; + color: currentColor; + border-radius: 4px; + height: 32px; + font-weight: 600; +} +.navbar-sub-nav > li > button, +.navbar-nav > li > button { + background: transparent; + border: 0; +} +.navbar-sub-nav .dropdown-menu, +.navbar-nav .dropdown-menu { + position: absolute; +} +.navbar-sub-nav { + display: flex; + margin: 0 0 0 6px; +} +.caret-down, +.btn .caret-down { + top: 0; + height: 11px; + width: 11px; + margin-left: 4px; + fill: currentColor; +} +.header-user .dropdown-menu, +.header-new .dropdown-menu { + margin-top: 4px; +} +.btn-sign-in { + background-color: #ebebfa; + color: #292961; + font-weight: 600; + line-height: 18px; + margin: 4px 0 4px 2px; +} +.title-container .badge.badge-pill, +.navbar-nav .badge.badge-pill { + position: inherit; + font-weight: 400; + margin-left: -6px; + font-size: 11px; + color: #333; + padding: 0 5px; + line-height: 12px; + border-radius: 7px; + box-shadow: 0 1px 0 rgba(76, 78, 84, 0.2); +} +.title-container .badge.badge-pill.green-badge, +.navbar-nav .badge.badge-pill.green-badge { + background-color: #1aaa55; +} +.title-container .badge.badge-pill.merge-requests-count, +.navbar-nav .badge.badge-pill.merge-requests-count { + background-color: #fca429; +} +.title-container .badge.badge-pill.todos-count, +.navbar-nav .badge.badge-pill.todos-count { + background-color: #1f78d1; +} +.title-container .canary-badge .badge, +.navbar-nav .canary-badge .badge { + font-size: 12px; + line-height: 16px; + padding: 0 0.5rem; +} + +@media (max-width: 575.98px) { + .navbar-gitlab .container-fluid { + font-size: 18px; + } + .navbar-gitlab .container-fluid .navbar-nav { + table-layout: fixed; + width: 100%; + margin: 0; + text-align: right; + } + .navbar-gitlab .container-fluid .navbar-collapse { + margin-left: -8px; + margin-right: -10px; + } + .navbar-gitlab .container-fluid .navbar-collapse .nav > li:not(.d-none) { + flex: 1; + } + .header-user-dropdown-toggle { + text-align: center; + } + .header-user-avatar { + float: none; + } +} +.header-user.show .dropdown-menu { + margin-top: 4px; + color: #fafafa; + left: auto; + max-height: 445px; +} +.header-user.show .dropdown-menu svg { + vertical-align: text-top; +} +.header-user-avatar { + float: left; + margin-right: 5px; + border-radius: 50%; + border: 1px solid #333; +} +.media { + display: flex; + align-items: flex-start; +} +.card { + margin-bottom: 16px; +} +.content-wrapper { + width: 100%; +} +.content-wrapper .container-fluid { + padding: 0 16px; +} + +@media (min-width: 768px) { + .page-with-contextual-sidebar { + padding-left: 50px; + } +} + +@media (min-width: 1200px) { + .page-with-contextual-sidebar { + padding-left: 220px; + } +} +.context-header { + position: relative; + margin-right: 2px; + width: 220px; +} +.context-header > a, +.context-header > button { + font-weight: 600; + display: flex; + width: 100%; + align-items: center; + padding: 10px 16px 10px 10px; + color: #fafafa; + background-color: transparent; + border: 0; + text-align: left; +} +.context-header .avatar-container { + flex: 0 0 40px; + background-color: #333; +} +.context-header .sidebar-context-title { + overflow: hidden; + text-overflow: ellipsis; +} +.context-header .sidebar-context-title.text-secondary { + font-weight: normal; + font-size: 0.8em; +} +.nav-sidebar { + position: fixed; + z-index: 600; + width: 220px; + top: 40px; + bottom: 0; + left: 0; + background-color: #2e2e2e; + box-shadow: inset -1px 0 0 #4f4f4f; + transform: translate3d(0, 0, 0); +} + +@media (min-width: 576px) and (max-width: 576px) { + .nav-sidebar:not(.sidebar-collapsed-desktop) { + box-shadow: inset -1px 0 0 #4f4f4f, 2px 1px 3px rgba(0, 0, 0, 0.1); + } +} +.nav-sidebar.sidebar-collapsed-desktop { + width: 50px; +} +.nav-sidebar.sidebar-collapsed-desktop .nav-sidebar-inner-scroll { + overflow-x: hidden; +} +.nav-sidebar.sidebar-collapsed-desktop .badge.badge-pill:not(.fly-out-badge), +.nav-sidebar.sidebar-collapsed-desktop .sidebar-context-title, +.nav-sidebar.sidebar-collapsed-desktop .nav-item-name { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; +} +.nav-sidebar.sidebar-collapsed-desktop .sidebar-top-level-items > li > a { + min-height: 45px; +} +.nav-sidebar.sidebar-collapsed-desktop .fly-out-top-item { + display: block; +} +.nav-sidebar.sidebar-collapsed-desktop .avatar-container { + margin: 0 auto; +} +.nav-sidebar.sidebar-expanded-mobile { + left: 0; +} +.nav-sidebar a { + text-decoration: none; +} +.nav-sidebar ul { + padding-left: 0; + list-style: none; +} +.nav-sidebar li { + white-space: nowrap; +} +.nav-sidebar li a { + display: flex; + align-items: center; + padding: 12px 16px; + color: #bababa; +} +.nav-sidebar li .nav-item-name { + flex: 1; +} +.nav-sidebar li.active > a { + font-weight: 600; +} + +@media (max-width: 767.98px) { + .nav-sidebar { + left: -220px; + } +} +.nav-sidebar .nav-icon-container { + display: flex; + margin-right: 8px; +} +.nav-sidebar .fly-out-top-item { + display: none; +} +.nav-sidebar svg { + height: 16px; + width: 16px; +} + +@media (min-width: 768px) and (max-width: 1199px) { + .nav-sidebar:not(.sidebar-expanded-mobile) { + width: 50px; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .nav-sidebar-inner-scroll { + overflow-x: hidden; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .badge.badge-pill:not(.fly-out-badge), + .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-context-title, + .nav-sidebar:not(.sidebar-expanded-mobile) .nav-item-name { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items > li > a { + min-height: 45px; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .fly-out-top-item { + display: block; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .avatar-container { + margin: 0 auto; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .context-header { + height: 60px; + width: 50px; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .context-header a { + padding: 10px 4px; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items > li .sidebar-sub-level-items:not(.flyout-list) { + display: none; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .nav-icon-container { + margin-right: 0; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button { + padding: 16px; + width: 49px; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .collapse-text, + .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .icon-chevron-double-lg-left { + display: none; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .icon-chevron-double-lg-right { + display: block; + margin: 0; + } +} +.nav-sidebar-inner-scroll { + height: 100%; + width: 100%; + overflow: auto; +} +.sidebar-sub-level-items { + display: none; + padding-bottom: 8px; +} +.sidebar-sub-level-items > li a { + padding: 8px 16px 8px 40px; +} +.sidebar-top-level-items { + margin-bottom: 60px; +} + +@media (min-width: 576px) { + .sidebar-top-level-items > li > a { + margin-right: 1px; + } +} +.sidebar-top-level-items > li .badge.badge-pill { + background-color: rgba(255, 255, 255, 0.08); + color: #bababa; +} +.sidebar-top-level-items > li.active { + background: rgba(255, 255, 255, 0.04); +} +.sidebar-top-level-items > li.active > a { + margin-left: 4px; + padding-left: 12px; +} +.sidebar-top-level-items > li.active .badge.badge-pill { + font-weight: 600; +} +.sidebar-top-level-items > li.active .sidebar-sub-level-items:not(.is-fly-out-only) { + display: block; +} +.toggle-sidebar-button, +.close-nav-button { + width: 219px; + position: fixed; + height: 48px; + bottom: 0; + padding: 0 16px; + background-color: #2e2e2e; + border: 0; + border-top: 1px solid #4f4f4f; + color: #bababa; + display: flex; + align-items: center; +} +.toggle-sidebar-button svg, +.close-nav-button svg { + margin-right: 8px; +} +.toggle-sidebar-button .icon-chevron-double-lg-right, +.close-nav-button .icon-chevron-double-lg-right { + display: none; +} +.collapse-text { + white-space: nowrap; + overflow: hidden; +} +.sidebar-collapsed-desktop .context-header { + height: 60px; + width: 50px; +} +.sidebar-collapsed-desktop .context-header a { + padding: 10px 4px; +} +.sidebar-collapsed-desktop .sidebar-top-level-items > li .sidebar-sub-level-items:not(.flyout-list) { + display: none; +} +.sidebar-collapsed-desktop .nav-icon-container { + margin-right: 0; +} +.sidebar-collapsed-desktop .toggle-sidebar-button { + padding: 16px; + width: 49px; +} +.sidebar-collapsed-desktop .toggle-sidebar-button .collapse-text, +.sidebar-collapsed-desktop .toggle-sidebar-button .icon-chevron-double-lg-left { + display: none; +} +.sidebar-collapsed-desktop .toggle-sidebar-button .icon-chevron-double-lg-right { + display: block; + margin: 0; +} +.fly-out-top-item > a { + display: flex; +} +.fly-out-top-item .fly-out-badge { + margin-left: 8px; +} +.fly-out-top-item-name { + flex: 1; +} +.close-nav-button { + display: none; +} + +@media (max-width: 767.98px) { + .close-nav-button { + display: flex; + } + .toggle-sidebar-button { + display: none; + } +} +table.table { + margin-bottom: 16px; +} +table.table .dropdown-menu a { + text-decoration: none; +} +table.table .success, +table.table .info { + color: #333; +} +table.table .success a:not(.btn), +table.table .info a:not(.btn) { + text-decoration: underline; + color: #333; +} +pre { + font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace; + display: block; + padding: 8px 12px; + margin: 0 0 8px; + font-size: 13px; + word-break: break-all; + word-wrap: break-word; + color: #fafafa; + background-color: #2e2e2e; + border: 1px solid #4f4f4f; + border-radius: 2px; +} +.monospace { + font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace; +} +input::-moz-placeholder, +textarea::-moz-placeholder { + color: #a7a7a7; + opacity: 1; +} +input::-ms-input-placeholder, +textarea::-ms-input-placeholder { + color: #a7a7a7; +} +input:-ms-input-placeholder, +textarea:-ms-input-placeholder { + color: #a7a7a7; +} +svg { + fill: currentColor; +} + +svg.s12 { + width: 12px; + height: 12px; +} + +svg.s16 { + width: 16px; + height: 16px; +} + +svg.s18 { + width: 18px; + height: 18px; +} + +svg.s12 { + vertical-align: -1px; +} + +svg.s16 { + vertical-align: -3px; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +table.code { + width: 100%; + font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace; + border: 0; + border-collapse: separate; + margin: 0; + padding: 0; + table-layout: fixed; + border-radius: 0 0 4px 4px; +} +.frame .badge.badge-pill { + position: absolute; + background-color: #1b69b6; + color: #333; + border: #333 1px solid; + min-height: 16px; + padding: 5px 8px; + border-radius: 12px; +} +.frame .badge.badge-pill { + transform: translate(-50%, -50%); +} +.color-label { + padding: 0 0.5rem; + line-height: 16px; + border-radius: 100px; + color: #333; +} +.label-link { + display: inline-flex; + vertical-align: text-bottom; +} +.milestones { + padding: 8px; + margin-top: 8px; + border-radius: 4px; + background-color: #4f4f4f; +} +.search { + margin: 0 8px; +} +.search form { + margin: 0; + padding: 4px; + width: 200px; + line-height: 24px; + height: 32px; + border: 0; + border-radius: 4px; +} + +@media (min-width: 1200px) { + .search form { + width: 320px; + } +} +.search .search-input { + border: 0; + font-size: 14px; + padding: 0 20px 0 0; + margin-left: 5px; + line-height: 25px; + width: 98%; + color: #333; + background: none; +} +.search .search-input-container { + display: flex; + position: relative; +} +.search .search-input-wrap { + width: 100%; +} +.search .search-input-wrap .search-icon, +.search .search-input-wrap .clear-icon { + position: absolute; + right: 5px; + top: 4px; +} +.search .search-input-wrap .search-icon { + -moz-user-select: none; + user-select: none; +} +.search .search-input-wrap .clear-icon { + display: none; +} +.search .search-input-wrap .dropdown { + position: static; +} +.search .search-input-wrap .dropdown-menu { + left: -5px; + max-height: 400px; + overflow: auto; +} + +@media (min-width: 1200px) { + .search .search-input-wrap .dropdown-menu { + width: 320px; + } +} +.search .search-input-wrap .dropdown-content { + max-height: 382px; +} +.settings { + border-top: 1px solid #4f4f4f; +} +.settings:first-of-type { + margin-top: 10px; + border: 0; +} +.settings + div .settings:first-of-type { + margin-top: 0; + border-top: 1px solid #4f4f4f; +} +.avatar, .avatar-container { + float: left; + margin-right: 16px; + border-radius: 50%; + border: 1px solid #333; +} +.s16.avatar, .s16.avatar-container { + width: 16px; + height: 16px; + margin-right: 8px; +} +.s18.avatar, .s18.avatar-container { + width: 18px; + height: 18px; + margin-right: 8px; +} +.s40.avatar, .s40.avatar-container { + width: 40px; + height: 40px; + margin-right: 8px; +} +.avatar { + transition-property: none; + width: 40px; + height: 40px; + padding: 0; + background: #222; + overflow: hidden; + border-color: rgba(255, 255, 255, 0.1); +} +.avatar.center { + font-size: 14px; + line-height: 1.8em; + text-align: center; +} +.avatar.avatar-tile { + border-radius: 0; + border: 0; +} +.avatar-container { + overflow: hidden; + display: flex; +} +.avatar-container a { + width: 100%; + height: 100%; + display: flex; + text-decoration: none; +} +.avatar-container .avatar { + border-radius: 0; + border: 0; + height: auto; + width: 100%; + margin: 0; + align-self: center; +} +.avatar-container.s40 { + min-width: 40px; + min-height: 40px; +} +.rect-avatar { + border-radius: 2px; +} +.rect-avatar.s16 { + border-radius: 2px; +} +.rect-avatar.s18 { + border-radius: 2px; +} +.rect-avatar.s40 { + border-radius: 4px; +} +.tab-width-8 { + -moz-tab-size: 8; + tab-size: 8; +} +.gl-sr-only { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; +} +.gl-ml-3 { + margin-left: 0.5rem; +} +.content-wrapper > .alert-wrapper, +#content-body, .modal-dialog { + display: block; +} +@import 'cloaking'; +@include cloak-startup-scss(none); diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss index 2a7a9255ded..a98e91b32eb 100644 --- a/app/assets/stylesheets/startup/startup-general.scss +++ b/app/assets/stylesheets/startup/startup-general.scss @@ -1,5 +1,1833 @@ -@charset "UTF-8";*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;overflow-y:scroll}header,nav{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans",Ubuntu,Cantarell,"Helvetica Neue",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-weight:400;line-height:1.5;color:#303030;text-align:left;background-color:#fff}hr{box-sizing:content-box;height:0;margin-top:.5rem;margin-bottom:.5rem;border:0;border-top:1px solid rgba(0,0,0,.1);overflow:hidden;margin:24px 0;border-top:1px solid #eee}p,ul{margin-top:0;margin-bottom:1rem}ul ul{margin-bottom:0}strong{font-weight:700}a{text-decoration:none;background-color:transparent;color:#1068bf}a:not([href]){color:inherit;text-decoration:none}code{font-family:"Menlo","DejaVu Sans Mono","Liberation Mono","Consolas","Ubuntu Mono","Courier New","andale mono","lucida console",monospace;font-size:90%;word-wrap:break-word;padding:2px 4px;color:#1f1f1f;background-color:#f0f0f0;border-radius:4px}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:baseline;fill:currentColor}button{border-radius:0;text-transform:none}button,input{margin:0;font-family:inherit;font-size:inherit;line-height:inherit;overflow:visible}[type=button]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}[type=search]{outline-offset:-2px}summary{display:list-item;cursor:pointer}[hidden]{display:none!important}.h1,h1{margin-bottom:.25rem;font-weight:600;line-height:1.2;color:#303030;font-size:2.1875rem}.list-unstyled{padding-left:0;list-style:none}a>code{color:inherit}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.search form{display:block;padding:.375rem .75rem;font-weight:400;color:#303030;background-color:#fff;background-clip:padding-box;border-radius:.25rem}.search form::-ms-expand{background-color:transparent;border:0}.search form:-moz-focusring{color:transparent;text-shadow:0 0 0 #303030}.search form::placeholder{opacity:1;color:#919191}.search form:disabled{background-color:#fafafa;opacity:1}.form-inline{display:flex;flex-flow:row wrap;align-items:center}@media (min-width:576px){.form-inline .search form,.search .form-inline form{display:inline-block;width:auto;vertical-align:middle}}.btn{display:inline-block;text-align:center;vertical-align:middle;cursor:pointer;user-select:none;border:1px solid transparent;padding:.375rem .75rem;line-height:20px;border-radius:.25rem}.btn:disabled{opacity:.65}.btn-success{color:#fff;background-color:#108548;border-color:#108548}.btn-success:disabled{color:#fff;background-color:#108548;border-color:#108548}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-menu-toggle{color:#fff;background-color:#0b572f;border-color:#094c29}.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.collapse:not(.show){display:none}.dropdown-menu-toggle::after{margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-menu-toggle:empty::after{margin-left:0}.dropdown-menu{left:0;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#303030;text-align:left;list-style:none;background-clip:padding-box;border:1px solid rgba(0,0,0,.15)}.dropdown-menu-right{right:0;left:auto}.divider{height:0;margin:4px 0;overflow:hidden;border-top:1px solid #dbdbdb}.dropdown-menu.show{display:block}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.navbar{position:relative;padding:.25rem .5rem}.navbar,.navbar .container,.navbar .container-fluid{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .dropdown-menu{float:none}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}.badge,.card{border-radius:.25rem}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid #dbdbdb}.card>hr{margin-right:0;margin-left:0}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:600;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.close{float:right;font-size:1.5rem;font-weight:600;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}button.close{padding:0;background-color:transparent;border:0;appearance:none}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dbdbdb!important}.rounded{border-radius:.25rem!important}.d-none{display:none!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}@media (min-width:576px){.d-sm-none{display:none!important}}@media (min-width:768px){.d-md-block{display:block!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-block{display:block!important}}@media (min-width:1200px){.d-xl-block{display:block!important}}.float-right{float:right!important}.sr-only{white-space:nowrap}.m-auto{margin:auto!important}.text-nowrap{white-space:nowrap!important}.search form,body{font-size:.875rem}[role=button],button,html [type=button]{cursor:pointer}.h1,h1{margin-top:20px;margin-bottom:10px}input[type=file]{line-height:1}.code>code{background-color:inherit;padding:unset}.hidden{display:none!important;visibility:hidden!important}.dropdown-menu-toggle::after,.hide{display:none}.badge:not(.gl-badge){padding:4px 5px;font-size:12px;font-style:normal;font-weight:400;display:inline-block}.toggle-sidebar-button .collapse-text,.toggle-sidebar-button .icon-chevron-double-lg-left,.toggle-sidebar-button .icon-chevron-double-lg-right{color:#707070}body{text-decoration-skip:ink}.container{padding-top:0;z-index:5}.container .content{margin:0}@media (max-width:575.98px){.container .content{margin-top:20px}.container .container .title{padding-left:15px!important}}.btn{border-radius:4px;font-size:.875rem;font-weight:400;padding:6px 10px;background-color:#fff;border-color:#dbdbdb;color:#303030;white-space:nowrap}.btn:active{box-shadow:none}.btn.active,.btn:active{box-shadow:rgba(0,0,0,.16);background-color:#eaeaea;border-color:#e3e3e3;color:#303030}.btn.btn-sm{padding:4px 10px;font-size:13px;line-height:18px}.btn.btn-success{background-color:#108548;border-color:#217645;color:#fff}.btn.btn-success.active,.btn.btn-success:active{box-shadow:rgba(0,0,0,.16);background-color:#24663b;border-color:#0d532a;color:#fff}.btn svg{height:15px;width:15px;position:relative;top:2px}.btn .fa:not(:last-child),.btn svg:not(:last-child){margin-right:5px}.badge.badge-pill:not(.gl-badge){font-weight:400;background-color:rgba(0,0,0,.07);color:#4f4f4f;vertical-align:baseline}.loading{margin:20px auto;height:40px;color:#555;font-size:32px;text-align:center}.chart{overflow:hidden;height:220px}.center{text-align:center}.flex{display:flex}.dropdown{position:relative}.show.dropdown .dropdown-menu{transform:translateY(0);display:block;min-height:40px;max-height:312px;overflow-y:auto}@media (max-width:575.98px){.show.dropdown .dropdown-menu{width:100%}}.show.dropdown .dropdown-menu-toggle{border-color:#c4c4c4}.search-input-container .dropdown-menu{margin-top:11px}.dropdown-menu,.dropdown-menu-toggle{font-size:14px;background-color:#fff;border:1px solid #dbdbdb;border-radius:.25rem}.dropdown-menu-toggle{color:#303030;text-align:left;white-space:nowrap;padding:6px 25px 6px 10px;position:relative;width:160px;text-overflow:ellipsis;overflow:hidden}.no-outline.dropdown-menu-toggle,.show.dropdown [data-toggle=dropdown]{outline:0}.dropdown-menu-toggle .fa{color:#c4c4c4;position:absolute}.dropdown-menu{display:none;position:absolute;width:auto;top:100%;z-index:300;min-width:240px;max-width:500px;margin-top:4px;margin-bottom:24px;font-weight:400;padding:8px 0;box-shadow:0 2px 4px rgba(0,0,0,.1)}.dropdown-menu ul{margin:0;padding:0}.dropdown-menu li{display:block;text-align:left;list-style:none;padding:0 1px}.dropdown-menu li button,.dropdown-menu li>a{background:0 0;border:0;border-radius:0;box-shadow:none;display:block;font-weight:400;position:relative;padding:8px 12px;color:#303030;line-height:16px;white-space:normal;overflow:hidden;text-align:left;width:100%}.dropdown-menu li button:active,.dropdown-menu li>a:active{background-color:#eee;color:#303030;outline:0;text-decoration:none}.dropdown-menu li button:active .avatar,.dropdown-menu li>a:active .avatar{border-color:#fff}.dropdown-menu li button:active .badge.badge-pill,.dropdown-menu li>a:active .badge.badge-pill{background-color:#d3e7f9}.dropdown-menu .divider{height:1px;margin:.25rem 0;padding:0;background-color:#dbdbdb}.dropdown-menu .badge.badge-pill+span:not(.badge.badge-pill){margin-right:40px}.dropdown-select{width:300px}@media (max-width:767.98px){.dropdown-select{width:100%}}.dropdown-content{max-height:252px;overflow-y:auto}.dropdown-loading{position:absolute;top:0;right:0;bottom:0;left:0;display:none;z-index:9;background-color:rgba(255,255,255,.6);font-size:28px}.dropdown-loading .fa{position:absolute;top:50%;left:50%;margin-top:-14px;margin-left:-14px}@media (max-width:575.98px){.navbar-gitlab li.dropdown{position:static}header.navbar-gitlab .dropdown .dropdown-menu{width:100%;min-width:100%}}@media (max-width:767.98px){.dropdown-menu-toggle{width:100%}}input{border-radius:.25rem;color:#303030;background-color:#fff}.search form{margin:0;padding:4px;width:200px;line-height:24px;height:32px;border:0;border-radius:4px}body.ui-indigo .navbar-gitlab{background-color:#292961}body.ui-indigo .navbar-gitlab .nav>li,body.ui-indigo .navbar-gitlab .navbar-collapse,body.ui-indigo .navbar-gitlab .navbar-sub-nav{color:#d1d1f0}body.ui-indigo .navbar-gitlab .container-fluid .navbar-toggler{border-left:1px solid #6868b9}body.ui-indigo .navbar-gitlab .container-fluid .navbar-toggler svg{fill:#d1d1f0}body.ui-indigo .navbar-gitlab .nav>li.active>a,body.ui-indigo .navbar-gitlab .nav>li.dropdown.show>a,body.ui-indigo .navbar-gitlab .navbar-nav>li.active>a,body.ui-indigo .navbar-gitlab .navbar-nav>li.active>button,body.ui-indigo .navbar-gitlab .navbar-nav>li.dropdown.show>a,body.ui-indigo .navbar-gitlab .navbar-nav>li.dropdown.show>button,body.ui-indigo .navbar-gitlab .navbar-sub-nav>li.active>a,body.ui-indigo .navbar-gitlab .navbar-sub-nav>li.active>button,body.ui-indigo .navbar-gitlab .navbar-sub-nav>li.dropdown.show>a,body.ui-indigo .navbar-gitlab .navbar-sub-nav>li.dropdown.show>button{color:#292961;background-color:#fff}body.ui-indigo .navbar-gitlab .nav>li>a.header-user-dropdown-toggle .header-user-avatar{border-color:#d1d1f0}body.ui-indigo .search form{background-color:rgba(209,209,240,.2)}body.ui-indigo .search .search-input::placeholder{color:rgba(209,209,240,.8)}body.ui-indigo .search .search-input-wrap .clear-icon,body.ui-indigo .search .search-input-wrap .search-icon{fill:rgba(209,209,240,.8)}body.ui-indigo .nav-sidebar li.active{box-shadow:inset 4px 0 0 #4b4ba3}body.ui-indigo .nav-sidebar li.active>a,body.ui-indigo .sidebar-top-level-items>li.active .badge.badge-pill{color:#393982}body.ui-indigo .nav-sidebar li.active .nav-icon-container svg{fill:#393982}.navbar-gitlab{padding:0 16px;z-index:1000;margin-bottom:0;min-height:40px;border:0;border-bottom:1px solid #dbdbdb;position:fixed;top:0;left:0;right:0;border-radius:0}.navbar-gitlab .logo-text{line-height:initial}.navbar-gitlab .logo-text svg{width:55px;height:14px;margin:0;fill:#fff}.navbar-gitlab .close-icon{display:none}.navbar-gitlab .header-content{width:100%;display:flex;justify-content:space-between;position:relative;min-height:40px;padding-left:0}.navbar-gitlab .header-content .title-container{display:flex;align-items:stretch;flex:1 1 auto;padding-top:0;overflow:visible}.navbar-gitlab .header-content .title{padding-right:0;color:currentColor;display:flex;position:relative;margin:0;font-size:18px;vertical-align:top;white-space:nowrap}.navbar-gitlab .header-content .title img{height:28px}.navbar-gitlab .header-content .title img+.logo-text{margin-left:8px}.navbar-gitlab .header-content .title a{display:flex;align-items:center;padding:2px 8px;margin:5px 2px 5px -8px;border-radius:4px}.navbar-gitlab .header-content .dropdown.open>a{border-bottom-color:#fff}.navbar-gitlab .header-content .navbar-collapse>ul.nav>li:not(.d-none){margin:0 2px}.navbar-gitlab .navbar-collapse{flex:0 0 auto;border-top:0;padding:0}@media (max-width:575.98px){.navbar-gitlab .navbar-collapse{flex:1 1 auto}}.navbar-gitlab .navbar-collapse .nav{flex-wrap:nowrap}@media (max-width:575.98px){.navbar-gitlab .navbar-collapse .nav>li:not(.d-none) a{margin-left:0}}.navbar-gitlab .container-fluid{padding:0}.navbar-gitlab .container-fluid .user-counter svg{margin-right:3px}.navbar-gitlab .container-fluid .navbar-toggler{position:relative;right:-10px;border-radius:0;min-width:45px;padding:0;margin:8px -7px 8px 0;font-size:14px;text-align:center;color:currentColor}.navbar-gitlab .container-fluid .navbar-toggler.active{color:currentColor;background-color:transparent}@media (max-width:575.98px){.navbar-gitlab .container-fluid .navbar-nav{display:flex;padding-right:10px;flex-direction:row}}.navbar-gitlab .container-fluid .navbar-nav li .badge.badge-pill{box-shadow:none;font-weight:600}@media (max-width:575.98px){.navbar-gitlab .container-fluid .nav>li.header-user{padding-left:10px}}.navbar-gitlab .container-fluid .nav>li>a{will-change:color;margin:4px 0;padding:6px 8px;height:32px}@media (max-width:575.98px){.navbar-gitlab .container-fluid .nav>li>a{padding:0}}.navbar-gitlab .container-fluid .nav>li>a.header-user-dropdown-toggle{margin-left:2px}.navbar-gitlab .container-fluid .nav>li .header-new-dropdown-toggle,.navbar-gitlab .container-fluid .nav>li>a.header-user-dropdown-toggle .header-user-avatar{margin-right:0}.navbar-nav>li>a,.navbar-nav>li>button,.navbar-sub-nav>li>a,.navbar-sub-nav>li>button{display:flex;align-items:center;justify-content:center;padding:6px 8px;margin:4px 2px;font-size:12px;color:currentColor;border-radius:4px;height:32px;font-weight:600}.navbar-nav>li>button,.navbar-sub-nav>li>button{background:0 0;border:0}.navbar-nav .dropdown-menu,.navbar-sub-nav .dropdown-menu{position:absolute}.navbar-sub-nav{display:flex;margin:0 0 0 6px}.btn .caret-down,.caret-down{top:0;height:11px;width:11px;margin-left:4px;fill:currentColor}.header-new .dropdown-menu,.header-user .dropdown-menu{margin-top:4px}.btn-sign-in{background-color:#ebebfa;color:#292961;font-weight:600;line-height:18px;margin:4px 0 4px 2px}.navbar-nav .badge.badge-pill,.title-container .badge.badge-pill{position:inherit;font-weight:400;margin-left:-6px;font-size:11px;color:#fff;padding:0 5px;line-height:12px;border-radius:7px;box-shadow:0 1px 0 rgba(76,78,84,.2)}.navbar-nav .badge.badge-pill.green-badge,.title-container .badge.badge-pill.green-badge{background-color:#108548}.navbar-nav .badge.badge-pill.merge-requests-count,.title-container .badge.badge-pill.merge-requests-count{background-color:#de7e00}.navbar-nav .badge.badge-pill.todos-count,.title-container .badge.badge-pill.todos-count{background-color:#1f75cb}.navbar-nav .canary-badge .badge,.title-container .canary-badge .badge{font-size:12px;line-height:16px;padding:0 .5rem}@media (max-width:575.98px){.navbar-gitlab .container-fluid{font-size:18px}.navbar-gitlab .container-fluid .navbar-nav{table-layout:fixed;width:100%;margin:0;text-align:right}.navbar-gitlab .container-fluid .navbar-collapse{margin-left:-8px;margin-right:-10px}.navbar-gitlab .container-fluid .navbar-collapse .nav>li:not(.d-none){flex:1}.header-user-dropdown-toggle{text-align:center}.header-user-avatar{float:none}}.header-user.show .dropdown-menu{margin-top:4px;color:#303030;left:auto;max-height:445px}.header-user.show .dropdown-menu svg{vertical-align:text-top}.header-user-avatar{float:left;margin-right:5px;border-radius:50%;border:1px solid #f5f5f5}.media{display:flex;align-items:flex-start}.card{margin-bottom:16px}@media (min-width:768px){.page-with-contextual-sidebar{padding-left:50px}}@media (min-width:1200px){.page-with-contextual-sidebar{padding-left:220px}}.context-header{position:relative;margin-right:2px;width:220px}.context-header>a,.context-header>button{font-weight:600;display:flex;width:100%;align-items:center;padding:10px 16px 10px 10px;color:#303030;background-color:transparent;border:0;text-align:left}.context-header .avatar-container{flex:0 0 40px;background-color:#fff}.context-header .sidebar-context-title{overflow:hidden;text-overflow:ellipsis}.context-header .sidebar-context-title.text-secondary{font-weight:400;font-size:.8em}.nav-sidebar{position:fixed;z-index:600;width:220px;top:40px;bottom:0;left:0;background-color:#fafafa;box-shadow:inset -1px 0 0 #dbdbdb;transform:translate3d(0,0,0)}@media (min-width:576px) and (max-width:576px){.nav-sidebar:not(.sidebar-collapsed-desktop){box-shadow:inset -1px 0 0 #dbdbdb,2px 1px 3px rgba(0,0,0,.1)}}.nav-sidebar a{text-decoration:none}.nav-sidebar ul{padding-left:0;list-style:none}.nav-sidebar li{white-space:nowrap}.nav-sidebar li a{display:flex;align-items:center;padding:12px 16px;color:#707070}.nav-sidebar li .nav-item-name{flex:1}.nav-sidebar li.active>a,.sidebar-top-level-items>li.active .badge.badge-pill{font-weight:600}@media (max-width:767.98px){.nav-sidebar{left:-220px}}.nav-sidebar .nav-icon-container{display:flex;margin-right:8px}.nav-sidebar .fly-out-top-item{display:none}.nav-sidebar svg{height:16px;width:16px}@media (min-width:768px) and (max-width:1199px){.nav-sidebar:not(.sidebar-expanded-mobile){width:50px}.nav-sidebar:not(.sidebar-expanded-mobile) .nav-sidebar-inner-scroll{overflow-x:hidden}.nav-sidebar:not(.sidebar-expanded-mobile) .badge.badge-pill:not(.fly-out-badge),.nav-sidebar:not(.sidebar-expanded-mobile) .nav-item-name,.nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-context-title{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items>li>a{min-height:45px}.nav-sidebar:not(.sidebar-expanded-mobile) .fly-out-top-item{display:block}.nav-sidebar:not(.sidebar-expanded-mobile) .avatar-container{margin:0 auto}.nav-sidebar:not(.sidebar-expanded-mobile) .context-header{height:60px;width:50px}.nav-sidebar:not(.sidebar-expanded-mobile) .context-header a{padding:10px 4px}.nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items>li .sidebar-sub-level-items:not(.flyout-list),.nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .collapse-text,.nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .icon-chevron-double-lg-left{display:none}.nav-sidebar:not(.sidebar-expanded-mobile) .nav-icon-container{margin-right:0}.nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button{padding:16px;width:49px}.nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .icon-chevron-double-lg-right{display:block;margin:0}}.nav-sidebar-inner-scroll{height:100%;width:100%;overflow:auto}.sidebar-sub-level-items{display:none;padding-bottom:8px}.sidebar-sub-level-items>li a{padding:8px 16px 8px 40px}.sidebar-sub-level-items>li.active a,.sidebar-top-level-items>li.active{background:rgba(0,0,0,.04)}.sidebar-top-level-items{margin-bottom:60px}@media (min-width:576px){.sidebar-top-level-items>li>a{margin-right:1px}}.sidebar-top-level-items>li .badge.badge-pill{background-color:rgba(0,0,0,.08);color:#707070}.sidebar-top-level-items>li.active>a{margin-left:4px;padding-left:12px}.sidebar-top-level-items>li.active .sidebar-sub-level-items:not(.is-fly-out-only){display:block}.close-nav-button,.toggle-sidebar-button{width:219px;position:fixed;height:48px;bottom:0;padding:0 16px;background-color:#fafafa;border:0;border-top:1px solid #dbdbdb;color:#707070;display:flex;align-items:center}.close-nav-button svg,.toggle-sidebar-button svg{margin-right:8px}.close-nav-button .icon-chevron-double-lg-right,.toggle-sidebar-button .icon-chevron-double-lg-right{display:none}.collapse-text{white-space:nowrap;overflow:hidden}.fly-out-top-item>a{display:flex}.fly-out-top-item .fly-out-badge{margin-left:8px}.fly-out-top-item-name{flex:1}.close-nav-button{display:none}@media (max-width:767.98px){.close-nav-button{display:flex}.toggle-sidebar-button{display:none}}input::-moz-placeholder{color:#919191;opacity:1}input:-ms-input-placeholder,input::-ms-input-placeholder{color:#919191}svg.s12{width:12px;height:12px}svg.s16{width:16px;height:16px}svg.s18{width:18px;height:18px}.feature-highlight-popover-sub-content{padding:16px 12px}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.color-label{padding:0 .5rem;line-height:16px;border-radius:100px;color:#fff}.label-link{display:inline-flex;vertical-align:text-bottom}.milestones{padding:8px;margin-top:8px;border-radius:4px;background-color:#dbdbdb}.search{margin:0 8px}@media (min-width:1200px){.search form{width:320px}}.search .search-input{border:0;font-size:14px;padding:0 20px 0 0;margin-left:5px;line-height:25px;width:98%;color:#fff;background:0 0}.search .search-input-container{display:flex;position:relative}.search .search-input-wrap{width:100%}.search .search-input-wrap .clear-icon,.search .search-input-wrap .search-icon{position:absolute;right:5px;top:4px}.search .search-input-wrap .search-icon{-moz-user-select:none;user-select:none}.search .search-input-wrap .clear-icon{display:none}.search .search-input-wrap .dropdown{position:static}.search .search-input-wrap .dropdown-menu{left:-5px;max-height:400px;overflow:auto}@media (min-width:1200px){.search .search-input-wrap .dropdown-menu{width:320px}}.search .search-input-wrap .dropdown-content{max-height:382px}.search .identicon{flex-basis:16px;flex-shrink:0;margin-right:4px}.settings{border-top:1px solid #dbdbdb}.settings:first-of-type{margin-top:10px;border:0}.settings+div .settings:first-of-type{margin-top:0;border-top:1px solid #dbdbdb}.avatar,.avatar-container{float:left;margin-right:16px;border-radius:50%;border:1px solid #f5f5f5}.s16.avatar,.s16.avatar-container{width:16px;height:16px;margin-right:8px}.s18.avatar,.s18.avatar-container{width:18px;height:18px;margin-right:8px}.s40.avatar,.s40.avatar-container{width:40px;height:40px;margin-right:8px}.avatar{transition-property:none;width:40px;height:40px;padding:0;background:#fdfdfd;overflow:hidden;border-color:rgba(0,0,0,.1)}.avatar.center{font-size:14px;line-height:1.8em;text-align:center}.avatar.avatar-tile{border-radius:0;border:0}.identicon{text-align:center;vertical-align:top;color:#4f4f4f;background-color:#eee}.identicon.s16{font-size:10px;line-height:16px}.identicon.s40{font-size:16px;line-height:38px}.avatar-container{overflow:hidden;display:flex}.avatar-container a{width:100%;height:100%;display:flex;text-decoration:none}.avatar-container .avatar{border-radius:0;border:0;height:auto;width:100%;margin:0;align-self:center}.avatar-container.s40{min-width:40px;min-height:40px}.rect-avatar,.rect-avatar.s16,.rect-avatar.s18{border-radius:2px}.rect-avatar.s40{border-radius:4px}.tab-width-8{-moz-tab-size:8;tab-size:8}.gl-sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.gl-ml-3{margin-left:.5rem} +@charset "UTF-8"; +*, +*::before, +*::after { + box-sizing: border-box; +} +html { + font-family: sans-serif; + line-height: 1.15; +} + header, nav, section { + display: block; +} +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #303030; + text-align: left; + background-color: #fff; +} +h1, h2, h3 { + margin-top: 0; + margin-bottom: 0.25rem; +} +p { + margin-top: 0; + margin-bottom: 1rem; +} -/* Cloaking in order to prevent flickering of content */ +ul { + margin-top: 0; + margin-bottom: 1rem; +} + +ul ul { + margin-bottom: 0; +} + +strong { + font-weight: bolder; +} +sub { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} +sub { + bottom: -.25em; +} +a { + color: #007bff; + text-decoration: none; + background-color: transparent; +} +a:not([href]) { + color: inherit; + text-decoration: none; +} +pre, +code { + font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace; + font-size: 1em; +} +pre { + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; +} +img { + vertical-align: middle; + border-style: none; +} +svg { + overflow: hidden; + vertical-align: middle; +} +table { + border-collapse: collapse; +} +th { + text-align: inherit; +} +button { + border-radius: 0; +} +input, +button, +textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} +button, +input { + overflow: visible; +} +button { + text-transform: none; +} +button:not(:disabled), +[type="button"]:not(:disabled), +[type="reset"]:not(:disabled) { + cursor: pointer; +} +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner { + padding: 0; + border-style: none; +} +textarea { + overflow: auto; + resize: vertical; +} +[type="search"] { + outline-offset: -2px; +} +summary { + display: list-item; + cursor: pointer; +} +template { + display: none; +} +[hidden] { + display: none !important; +} +h1, h2, h3, +.h1, .h2, .h3 { + margin-bottom: 0.25rem; + font-weight: 600; + line-height: 1.2; + color: #303030; +} +h1, .h1 { + font-size: 2.1875rem; +} +h2, .h2 { + font-size: 1.75rem; +} +h3, .h3 { + font-size: 1.53125rem; +} +.list-unstyled { + padding-left: 0; + list-style: none; +} +code { + font-size: 90%; + color: #1f1f1f; + word-wrap: break-word; +} +a > code { + color: inherit; +} +pre { + display: block; + font-size: 90%; + color: #303030; +} +pre code { + font-size: inherit; + color: inherit; + word-break: normal; +} +.container { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container { + max-width: 1140px; + } +} +.container-fluid { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container { + max-width: 1140px; + } +} +.row { + display: flex; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; +} +.table { + width: 100%; + margin-bottom: 0.5rem; + color: #303030; +} +.table th, +.table td { + padding: 0.75rem; + vertical-align: top; + border-top: 1px solid #dbdbdb; +} + .search form { + display: block; + width: 100%; + height: 34px; + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + font-weight: 400; + line-height: 1.5; + color: #303030; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #dbdbdb; + border-radius: 0.25rem; +} + +@media (prefers-reduced-motion: reduce) { +} + .search form:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 #303030; +} + .search form::placeholder { + color: #5e5e5e; + opacity: 1; +} + .search form:disabled { + background-color: #fafafa; + opacity: 1; +} +.form-inline { + display: flex; + flex-flow: row wrap; + align-items: center; +} + +@media (min-width: 576px) { + .form-inline .search form, .search .form-inline form { + display: inline-block; + width: auto; + vertical-align: middle; + } +} +.btn { + display: inline-block; + font-weight: 400; + color: #303030; + text-align: center; + vertical-align: middle; + cursor: pointer; + user-select: none; + background-color: transparent; + border: 1px solid transparent; + padding: 0.375rem 0.75rem; + font-size: 1rem; + line-height: 20px; + border-radius: 0.25rem; +} + +@media (prefers-reduced-motion: reduce) { +} +.btn.disabled, .btn:disabled { + opacity: 0.65; +} +a.btn.disabled { + pointer-events: none; +} +.collapse:not(.show) { + display: none; +} + +.dropdown { + position: relative; +} + .dropdown-menu-toggle { + white-space: nowrap; +} + .dropdown-menu-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid; + border-right: 0.3em solid transparent; + border-bottom: 0; + border-left: 0.3em solid transparent; +} + .dropdown-menu-toggle:empty::after { + margin-left: 0; +} +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 10rem; + padding: 0.5rem 0; + margin: 0.125rem 0 0; + font-size: 1rem; + color: #303030; + text-align: left; + list-style: none; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0.25rem; +} +.dropdown-menu-right { + right: 0; + left: auto; +} + .divider { + height: 0; + margin: 4px 0; + overflow: hidden; + border-top: 1px solid #dbdbdb; +} +.dropdown-menu.show { + display: block; +} +.nav { + display: flex; + flex-wrap: wrap; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} +.navbar { + position: relative; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + padding: 0.25rem 0.5rem; +} +.navbar .container, +.navbar .container-fluid { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; +} +.navbar-nav { + display: flex; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} +.navbar-nav .dropdown-menu { + position: static; + float: none; +} +.navbar-collapse { + flex-basis: 100%; + flex-grow: 1; + align-items: center; +} +.navbar-toggler { + padding: 0.25rem 0.75rem; + font-size: 1.25rem; + line-height: 1; + background-color: transparent; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +@media (max-width: 575.98px) { + .navbar-expand-sm > .container, + .navbar-expand-sm > .container-fluid { + padding-right: 0; + padding-left: 0; + } +} + +@media (min-width: 576px) { + .navbar-expand-sm { + flex-flow: row nowrap; + justify-content: flex-start; + } + .navbar-expand-sm .navbar-nav { + flex-direction: row; + } + .navbar-expand-sm .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-sm > .container, + .navbar-expand-sm > .container-fluid { + flex-wrap: nowrap; + } + .navbar-expand-sm .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-sm .navbar-toggler { + display: none; + } +} +.card { + position: relative; + display: flex; + flex-direction: column; + min-width: 0; + word-wrap: break-word; + background-color: #fff; + background-clip: border-box; + border: 1px solid #dbdbdb; + border-radius: 0.25rem; +} +.badge { + display: inline-block; + padding: 0.25em 0.4em; + font-size: 75%; + font-weight: 600; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25rem; +} + +@media (prefers-reduced-motion: reduce) { +} +.badge:empty { + display: none; +} +.btn .badge { + position: relative; + top: -1px; +} +.badge-pill { + padding-right: 0.6em; + padding-left: 0.6em; + border-radius: 10rem; +} +.media { + display: flex; + align-items: flex-start; +} +.close { + float: right; + font-size: 1.5rem; + font-weight: 600; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + opacity: .5; +} +button.close { + padding: 0; + background-color: transparent; + border: 0; + appearance: none; +} +a.close.disabled { + pointer-events: none; +} +.modal-dialog { + position: relative; + width: auto; + margin: 0.5rem; + pointer-events: none; +} + +@media (min-width: 576px) { + .modal-dialog { + max-width: 500px; + margin: 1.75rem auto; + } +} +.bg-transparent { + background-color: transparent !important; +} +.border { + border: 1px solid #dbdbdb !important; +} +.border-top { + border-top: 1px solid #dbdbdb !important; +} +.border-right { + border-right: 1px solid #dbdbdb !important; +} +.border-bottom { + border-bottom: 1px solid #dbdbdb !important; +} +.border-left { + border-left: 1px solid #dbdbdb !important; +} +.rounded { + border-radius: 0.25rem !important; +} +.clearfix::after { + display: block; + clear: both; + content: ""; +} +.d-none { + display: none !important; +} +.d-inline-block { + display: inline-block !important; +} +.d-block { + display: block !important; +} + +@media (min-width: 576px) { + .d-sm-none { + display: none !important; + } +} + +@media (min-width: 768px) { + .d-md-block { + display: block !important; + } +} + +@media (min-width: 992px) { + .d-lg-none { + display: none !important; + } + .d-lg-block { + display: block !important; + } +} + +@media (min-width: 1200px) { + .d-xl-block { + display: block !important; + } +} +.flex-wrap { + flex-wrap: wrap !important; +} +.float-right { + float: right !important; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} +.m-auto { + margin: auto !important; +} +.text-nowrap { + white-space: nowrap !important; +} +.visible { + visibility: visible !important; +} + .search form.focus { + color: #303030; + background-color: #fff; + border-color: #80bdff; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} +.gl-badge { + display: inline-flex; + align-items: center; + font-size: 0.75rem; + font-weight: 400; + line-height: 1rem; + padding-top: 0.25rem; + padding-bottom: 0.25rem; + padding-left: 0.5rem; + padding-right: 0.5rem; + outline: none; +} +body, .search form, +.search form { + font-size: 0.875rem; +} +button, +html [type='button'], +[type='reset'], +[role='button'] { + cursor: pointer; +} +h1, +.h1, +h2, +.h2, +h3, +.h3 { + margin-top: 20px; + margin-bottom: 10px; +} +input[type='file'] { + line-height: 1; +} + +strong { + font-weight: bold; +} +a { + color: #1068bf; +} +code { + padding: 2px 4px; + color: #1f1f1f; + background-color: #f0f0f0; + border-radius: 4px; +} +.code > code { + background-color: inherit; + padding: unset; +} +table { + border-spacing: 0; +} +.hidden { + display: none !important; + visibility: hidden !important; +} +.hide { + display: none; +} + .dropdown-menu-toggle::after { + display: none; +} +.badge:not(.gl-badge) { + padding: 4px 5px; + font-size: 12px; + font-style: normal; + font-weight: 400; + display: inline-block; +} +pre code { + white-space: pre-wrap; +} +.toggle-sidebar-button .collapse-text, +.toggle-sidebar-button .icon-chevron-double-lg-left, +.toggle-sidebar-button .icon-chevron-double-lg-right { + color: #666; +} +svg { + vertical-align: baseline; +} +html { + overflow-y: scroll; +} +body { + text-decoration-skip: ink; +} +.content-wrapper { + margin-top: 40px; + padding-bottom: 100px; +} +.container { + padding-top: 0; + z-index: 5; +} +.container .content { + margin: 0; +} + +@media (max-width: 575.98px) { + .container .content { + margin-top: 20px; + } +} + +@media (max-width: 575.98px) { + .container .container .title { + padding-left: 15px !important; + } +} +.btn { + border-radius: 4px; + font-size: 0.875rem; + font-weight: 400; + padding: 6px 10px; + background-color: #fff; + border-color: #dbdbdb; + color: #303030; + color: #303030; + white-space: nowrap; +} +.btn:active, .btn.active { + box-shadow: rgba(0, 0, 0, 0.16); + background-color: #eaeaea; + border-color: #e3e3e3; + color: #303030; +} +.btn svg { + height: 15px; + width: 15px; +} +.btn svg:not(:last-child), +.btn .fa:not(:last-child) { + margin-right: 5px; +} +.badge.badge-pill:not(.gl-badge) { + font-weight: 400; + background-color: rgba(0, 0, 0, 0.07); + color: #525252; + vertical-align: baseline; +} +.hint { + font-style: italic; + color: #bfbfbf; +} +.bold { + font-weight: 600; +} +pre.wrap { + word-break: break-word; + white-space: pre-wrap; +} +table a code { + position: relative; + top: -2px; + margin-right: 3px; +} +.loading { + margin: 20px auto; + height: 40px; + color: #525252; + font-size: 32px; + text-align: center; +} +.highlight { + text-shadow: none; +} +.chart { + overflow: hidden; + height: 220px; +} +.break-word { + word-wrap: break-word; +} +.center { + text-align: center; +} +.block { + display: block; +} +.flex { + display: flex; +} +.flex-grow { + flex-grow: 1; +} +.dropdown { + position: relative; +} +.show.dropdown .dropdown-menu { + transform: translateY(0); + display: block; + min-height: 40px; + max-height: 312px; + overflow-y: auto; +} + +@media (max-width: 575.98px) { + .show.dropdown .dropdown-menu { + width: 100%; + } +} + .show.dropdown .dropdown-menu-toggle, +.show.dropdown .dropdown-menu-toggle { + border-color: #c4c4c4; +} +.show.dropdown [data-toggle='dropdown'] { + outline: 0; +} +.search-input-container .dropdown-menu { + margin-top: 11px; +} + .dropdown-menu-toggle { + padding: 6px 8px 6px 10px; + background-color: #fff; + color: #303030; + font-size: 14px; + text-align: left; + border: 1px solid #dbdbdb; + border-radius: 0.25rem; + white-space: nowrap; +} + .no-outline.dropdown-menu-toggle { + outline: 0; +} + .dropdown-menu-toggle .fa { + color: #c4c4c4; +} +.dropdown-menu-toggle { + padding-right: 25px; + position: relative; + width: 160px; + text-overflow: ellipsis; + overflow: hidden; +} +.dropdown-menu-toggle .fa { + position: absolute; +} +.dropdown-menu { + display: none; + position: absolute; + width: auto; + top: 100%; + z-index: 300; + min-width: 240px; + max-width: 500px; + margin-top: 4px; + margin-bottom: 24px; + font-size: 14px; + font-weight: 400; + padding: 8px 0; + background-color: #fff; + border: 1px solid #dbdbdb; + border-radius: 0.25rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} +.dropdown-menu ul { + margin: 0; + padding: 0; +} +.dropdown-menu li { + display: block; + text-align: left; + list-style: none; + padding: 0 1px; +} +.dropdown-menu li > a, +.dropdown-menu li button { + background: transparent; + border: 0; + border-radius: 0; + box-shadow: none; + display: block; + font-weight: 400; + position: relative; + padding: 8px 12px; + color: #303030; + line-height: 16px; + white-space: normal; + overflow: hidden; + text-align: left; + width: 100%; +} +.dropdown-menu .divider { + height: 1px; + margin: 0.25rem 0; + padding: 0; + background-color: #dbdbdb; +} +.dropdown-menu .badge.badge-pill + span:not(.badge.badge-pill) { + margin-right: 40px; +} +.dropdown-select { + width: 300px; +} + +@media (max-width: 767.98px) { + .dropdown-select { + width: 100%; + } +} +.dropdown-content { + max-height: 252px; + overflow-y: auto; +} +.dropdown-loading { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + display: none; + z-index: 9; + background-color: rgba(255, 255, 255, 0.6); + font-size: 28px; +} +.dropdown-loading .fa { + position: absolute; + top: 50%; + left: 50%; + margin-top: -14px; + margin-left: -14px; +} + +@media (max-width: 575.98px) { + .navbar-gitlab li.dropdown { + position: static; + } + header.navbar-gitlab .dropdown .dropdown-menu { + width: 100%; + min-width: 100%; + } +} + +@media (max-width: 767.98px) { + .dropdown-menu-toggle { + width: 100%; + } +} +textarea { + resize: vertical; +} +input { + border-radius: 0.25rem; + color: #303030; + background-color: #fff; +} + .search form { + border-radius: 4px; + padding: 6px 10px; +} + .search form::placeholder { + color: #868686; +} +.navbar-gitlab { + padding: 0 16px; + z-index: 1000; + margin-bottom: 0; + min-height: 40px; + border: 0; + border-bottom: 1px solid #dbdbdb; + position: fixed; + top: 0; + left: 0; + right: 0; + border-radius: 0; +} +.navbar-gitlab .logo-text { + line-height: initial; +} +.navbar-gitlab .logo-text svg { + width: 55px; + height: 14px; + margin: 0; + fill: #fff; +} +.navbar-gitlab .close-icon { + display: none; +} +.navbar-gitlab .header-content { + width: 100%; + display: flex; + justify-content: space-between; + position: relative; + min-height: 40px; + padding-left: 0; +} +.navbar-gitlab .header-content .title-container { + display: flex; + align-items: stretch; + flex: 1 1 auto; + padding-top: 0; + overflow: visible; +} +.navbar-gitlab .header-content .title { + padding-right: 0; + color: currentColor; + display: flex; + position: relative; + margin: 0; + font-size: 18px; + vertical-align: top; + white-space: nowrap; +} +.navbar-gitlab .header-content .title img { + height: 28px; +} +.navbar-gitlab .header-content .title img + .logo-text { + margin-left: 8px; +} +.navbar-gitlab .header-content .title.wrap { + white-space: normal; +} +.navbar-gitlab .header-content .title a { + display: flex; + align-items: center; + padding: 2px 8px; + margin: 5px 2px 5px -8px; + border-radius: 4px; +} +.navbar-gitlab .header-content .dropdown.open > a { + border-bottom-color: #fff; +} +.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) { + margin: 0 2px; +} +.navbar-gitlab .navbar-collapse { + flex: 0 0 auto; + border-top: 0; + padding: 0; +} + +@media (max-width: 575.98px) { + .navbar-gitlab .navbar-collapse { + flex: 1 1 auto; + } +} +.navbar-gitlab .navbar-collapse .nav { + flex-wrap: nowrap; +} + +@media (max-width: 575.98px) { + .navbar-gitlab .navbar-collapse .nav > li:not(.d-none) a { + margin-left: 0; + } +} +.navbar-gitlab .container-fluid { + padding: 0; +} +.navbar-gitlab .container-fluid .user-counter svg { + margin-right: 3px; +} +.navbar-gitlab .container-fluid .navbar-toggler { + position: relative; + right: -10px; + border-radius: 0; + min-width: 45px; + padding: 0; + margin: 8px -7px 8px 0; + font-size: 14px; + text-align: center; + color: currentColor; +} + +@media (max-width: 575.98px) { + .navbar-gitlab .container-fluid .navbar-nav { + display: flex; + padding-right: 10px; + flex-direction: row; + } +} +.navbar-gitlab .container-fluid .navbar-nav li .badge.badge-pill { + box-shadow: none; + font-weight: 600; +} + +@media (max-width: 575.98px) { + .navbar-gitlab .container-fluid .nav > li.header-user { + padding-left: 10px; + } +} +.navbar-gitlab .container-fluid .nav > li > a { + will-change: color; + margin: 4px 0; + padding: 6px 8px; + height: 32px; +} + +@media (max-width: 575.98px) { + .navbar-gitlab .container-fluid .nav > li > a { + padding: 0; + } +} +.navbar-gitlab .container-fluid .nav > li > a.header-user-dropdown-toggle { + margin-left: 2px; +} +.navbar-gitlab .container-fluid .nav > li > a.header-user-dropdown-toggle .header-user-avatar { + margin-right: 0; +} +.navbar-gitlab .container-fluid .nav > li .header-new-dropdown-toggle { + margin-right: 0; +} +.navbar-sub-nav > li > a, +.navbar-sub-nav > li > button, +.navbar-nav > li > a, +.navbar-nav > li > button { + display: flex; + align-items: center; + justify-content: center; + padding: 6px 8px; + margin: 4px 2px; + font-size: 12px; + color: currentColor; + border-radius: 4px; + height: 32px; + font-weight: 600; +} +.navbar-sub-nav > li > button, +.navbar-nav > li > button { + background: transparent; + border: 0; +} +.navbar-sub-nav .dropdown-menu, +.navbar-nav .dropdown-menu { + position: absolute; +} +.navbar-sub-nav { + display: flex; + margin: 0 0 0 6px; +} +.caret-down, +.btn .caret-down { + top: 0; + height: 11px; + width: 11px; + margin-left: 4px; + fill: currentColor; +} +.header-user .dropdown-menu, +.header-new .dropdown-menu { + margin-top: 4px; +} +.btn-sign-in { + background-color: #ebebfa; + color: #292961; + font-weight: 600; + line-height: 18px; + margin: 4px 0 4px 2px; +} +.title-container .badge.badge-pill, +.navbar-nav .badge.badge-pill { + position: inherit; + font-weight: 400; + margin-left: -6px; + font-size: 11px; + color: #fff; + padding: 0 5px; + line-height: 12px; + border-radius: 7px; + box-shadow: 0 1px 0 rgba(76, 78, 84, 0.2); +} +.title-container .badge.badge-pill.green-badge, +.navbar-nav .badge.badge-pill.green-badge { + background-color: #108548; +} +.title-container .badge.badge-pill.merge-requests-count, +.navbar-nav .badge.badge-pill.merge-requests-count { + background-color: #de7e00; +} +.title-container .badge.badge-pill.todos-count, +.navbar-nav .badge.badge-pill.todos-count { + background-color: #1f75cb; +} +.title-container .canary-badge .badge, +.navbar-nav .canary-badge .badge { + font-size: 12px; + line-height: 16px; + padding: 0 0.5rem; +} + +@media (max-width: 575.98px) { + .navbar-gitlab .container-fluid { + font-size: 18px; + } + .navbar-gitlab .container-fluid .navbar-nav { + table-layout: fixed; + width: 100%; + margin: 0; + text-align: right; + } + .navbar-gitlab .container-fluid .navbar-collapse { + margin-left: -8px; + margin-right: -10px; + } + .navbar-gitlab .container-fluid .navbar-collapse .nav > li:not(.d-none) { + flex: 1; + } + .header-user-dropdown-toggle { + text-align: center; + } + .header-user-avatar { + float: none; + } +} +.header-user.show .dropdown-menu { + margin-top: 4px; + color: #303030; + left: auto; + max-height: 445px; +} +.header-user.show .dropdown-menu svg { + vertical-align: text-top; +} +.header-user-avatar { + float: left; + margin-right: 5px; + border-radius: 50%; + border: 1px solid #f5f5f5; +} +.media { + display: flex; + align-items: flex-start; +} +.card { + margin-bottom: 16px; +} +.content-wrapper { + width: 100%; +} +.content-wrapper .container-fluid { + padding: 0 16px; +} + +@media (min-width: 768px) { + .page-with-contextual-sidebar { + padding-left: 50px; + } +} + +@media (min-width: 1200px) { + .page-with-contextual-sidebar { + padding-left: 220px; + } +} +.context-header { + position: relative; + margin-right: 2px; + width: 220px; +} +.context-header > a, +.context-header > button { + font-weight: 600; + display: flex; + width: 100%; + align-items: center; + padding: 10px 16px 10px 10px; + color: #303030; + background-color: transparent; + border: 0; + text-align: left; +} +.context-header .avatar-container { + flex: 0 0 40px; + background-color: #fff; +} +.context-header .sidebar-context-title { + overflow: hidden; + text-overflow: ellipsis; +} +.context-header .sidebar-context-title.text-secondary { + font-weight: normal; + font-size: 0.8em; +} +.nav-sidebar { + position: fixed; + z-index: 600; + width: 220px; + top: 40px; + bottom: 0; + left: 0; + background-color: #fafafa; + box-shadow: inset -1px 0 0 #dbdbdb; + transform: translate3d(0, 0, 0); +} + +@media (min-width: 576px) and (max-width: 576px) { + .nav-sidebar:not(.sidebar-collapsed-desktop) { + box-shadow: inset -1px 0 0 #dbdbdb, 2px 1px 3px rgba(0, 0, 0, 0.1); + } +} +.nav-sidebar.sidebar-collapsed-desktop { + width: 50px; +} +.nav-sidebar.sidebar-collapsed-desktop .nav-sidebar-inner-scroll { + overflow-x: hidden; +} +.nav-sidebar.sidebar-collapsed-desktop .badge.badge-pill:not(.fly-out-badge), +.nav-sidebar.sidebar-collapsed-desktop .sidebar-context-title, +.nav-sidebar.sidebar-collapsed-desktop .nav-item-name { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; +} +.nav-sidebar.sidebar-collapsed-desktop .sidebar-top-level-items > li > a { + min-height: 45px; +} +.nav-sidebar.sidebar-collapsed-desktop .fly-out-top-item { + display: block; +} +.nav-sidebar.sidebar-collapsed-desktop .avatar-container { + margin: 0 auto; +} +.nav-sidebar.sidebar-expanded-mobile { + left: 0; +} +.nav-sidebar a { + text-decoration: none; +} +.nav-sidebar ul { + padding-left: 0; + list-style: none; +} +.nav-sidebar li { + white-space: nowrap; +} +.nav-sidebar li a { + display: flex; + align-items: center; + padding: 12px 16px; + color: #666; +} +.nav-sidebar li .nav-item-name { + flex: 1; +} +.nav-sidebar li.active > a { + font-weight: 600; +} + +@media (max-width: 767.98px) { + .nav-sidebar { + left: -220px; + } +} +.nav-sidebar .nav-icon-container { + display: flex; + margin-right: 8px; +} +.nav-sidebar .fly-out-top-item { + display: none; +} +.nav-sidebar svg { + height: 16px; + width: 16px; +} + +@media (min-width: 768px) and (max-width: 1199px) { + .nav-sidebar:not(.sidebar-expanded-mobile) { + width: 50px; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .nav-sidebar-inner-scroll { + overflow-x: hidden; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .badge.badge-pill:not(.fly-out-badge), + .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-context-title, + .nav-sidebar:not(.sidebar-expanded-mobile) .nav-item-name { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items > li > a { + min-height: 45px; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .fly-out-top-item { + display: block; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .avatar-container { + margin: 0 auto; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .context-header { + height: 60px; + width: 50px; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .context-header a { + padding: 10px 4px; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items > li .sidebar-sub-level-items:not(.flyout-list) { + display: none; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .nav-icon-container { + margin-right: 0; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button { + padding: 16px; + width: 49px; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .collapse-text, + .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .icon-chevron-double-lg-left { + display: none; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .icon-chevron-double-lg-right { + display: block; + margin: 0; + } +} +.nav-sidebar-inner-scroll { + height: 100%; + width: 100%; + overflow: auto; +} +.sidebar-sub-level-items { + display: none; + padding-bottom: 8px; +} +.sidebar-sub-level-items > li a { + padding: 8px 16px 8px 40px; +} +.sidebar-top-level-items { + margin-bottom: 60px; +} + +@media (min-width: 576px) { + .sidebar-top-level-items > li > a { + margin-right: 1px; + } +} +.sidebar-top-level-items > li .badge.badge-pill { + background-color: rgba(0, 0, 0, 0.08); + color: #666; +} +.sidebar-top-level-items > li.active { + background: rgba(0, 0, 0, 0.04); +} +.sidebar-top-level-items > li.active > a { + margin-left: 4px; + padding-left: 12px; +} +.sidebar-top-level-items > li.active .badge.badge-pill { + font-weight: 600; +} +.sidebar-top-level-items > li.active .sidebar-sub-level-items:not(.is-fly-out-only) { + display: block; +} +.toggle-sidebar-button, +.close-nav-button { + width: 219px; + position: fixed; + height: 48px; + bottom: 0; + padding: 0 16px; + background-color: #fafafa; + border: 0; + border-top: 1px solid #dbdbdb; + color: #666; + display: flex; + align-items: center; +} +.toggle-sidebar-button svg, +.close-nav-button svg { + margin-right: 8px; +} +.toggle-sidebar-button .icon-chevron-double-lg-right, +.close-nav-button .icon-chevron-double-lg-right { + display: none; +} +.collapse-text { + white-space: nowrap; + overflow: hidden; +} +.sidebar-collapsed-desktop .context-header { + height: 60px; + width: 50px; +} +.sidebar-collapsed-desktop .context-header a { + padding: 10px 4px; +} +.sidebar-collapsed-desktop .sidebar-top-level-items > li .sidebar-sub-level-items:not(.flyout-list) { + display: none; +} +.sidebar-collapsed-desktop .nav-icon-container { + margin-right: 0; +} +.sidebar-collapsed-desktop .toggle-sidebar-button { + padding: 16px; + width: 49px; +} +.sidebar-collapsed-desktop .toggle-sidebar-button .collapse-text, +.sidebar-collapsed-desktop .toggle-sidebar-button .icon-chevron-double-lg-left { + display: none; +} +.sidebar-collapsed-desktop .toggle-sidebar-button .icon-chevron-double-lg-right { + display: block; + margin: 0; +} +.fly-out-top-item > a { + display: flex; +} +.fly-out-top-item .fly-out-badge { + margin-left: 8px; +} +.fly-out-top-item-name { + flex: 1; +} +.close-nav-button { + display: none; +} + +@media (max-width: 767.98px) { + .close-nav-button { + display: flex; + } + .toggle-sidebar-button { + display: none; + } +} +table.table { + margin-bottom: 16px; +} +table.table .dropdown-menu a { + text-decoration: none; +} +table.table .success, +table.table .info { + color: #fff; +} +table.table .success a:not(.btn), +table.table .info a:not(.btn) { + text-decoration: underline; + color: #fff; +} +pre { + font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace; + display: block; + padding: 8px 12px; + margin: 0 0 8px; + font-size: 13px; + word-break: break-all; + word-wrap: break-word; + color: #303030; + background-color: #fafafa; + border: 1px solid #dbdbdb; + border-radius: 2px; +} +.monospace { + font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace; +} +input::-moz-placeholder, +textarea::-moz-placeholder { + color: #868686; + opacity: 1; +} +input::-ms-input-placeholder, +textarea::-ms-input-placeholder { + color: #868686; +} +input:-ms-input-placeholder, +textarea:-ms-input-placeholder { + color: #868686; +} +svg { + fill: currentColor; +} + +svg.s12 { + width: 12px; + height: 12px; +} + +svg.s16 { + width: 16px; + height: 16px; +} + +svg.s18 { + width: 18px; + height: 18px; +} + +svg.s12 { + vertical-align: -1px; +} + +svg.s16 { + vertical-align: -3px; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +table.code { + width: 100%; + font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace; + border: 0; + border-collapse: separate; + margin: 0; + padding: 0; + table-layout: fixed; + border-radius: 0 0 4px 4px; +} +.frame .badge.badge-pill { + position: absolute; + background-color: #428fdc; + color: #fff; + border: #fff 1px solid; + min-height: 16px; + padding: 5px 8px; + border-radius: 12px; +} +.frame .badge.badge-pill { + transform: translate(-50%, -50%); +} +.color-label { + padding: 0 0.5rem; + line-height: 16px; + border-radius: 100px; + color: #fff; +} +.label-link { + display: inline-flex; + vertical-align: text-bottom; +} +.milestones { + padding: 8px; + margin-top: 8px; + border-radius: 4px; + background-color: #dbdbdb; +} +.search { + margin: 0 8px; +} +.search form { + margin: 0; + padding: 4px; + width: 200px; + line-height: 24px; + height: 32px; + border: 0; + border-radius: 4px; +} + +@media (min-width: 1200px) { + .search form { + width: 320px; + } +} +.search .search-input { + border: 0; + font-size: 14px; + padding: 0 20px 0 0; + margin-left: 5px; + line-height: 25px; + width: 98%; + color: #fff; + background: none; +} +.search .search-input-container { + display: flex; + position: relative; +} +.search .search-input-wrap { + width: 100%; +} +.search .search-input-wrap .search-icon, +.search .search-input-wrap .clear-icon { + position: absolute; + right: 5px; + top: 4px; +} +.search .search-input-wrap .search-icon { + -moz-user-select: none; + user-select: none; +} +.search .search-input-wrap .clear-icon { + display: none; +} +.search .search-input-wrap .dropdown { + position: static; +} +.search .search-input-wrap .dropdown-menu { + left: -5px; + max-height: 400px; + overflow: auto; +} + +@media (min-width: 1200px) { + .search .search-input-wrap .dropdown-menu { + width: 320px; + } +} +.search .search-input-wrap .dropdown-content { + max-height: 382px; +} +.settings { + border-top: 1px solid #dbdbdb; +} +.settings:first-of-type { + margin-top: 10px; + border: 0; +} +.settings + div .settings:first-of-type { + margin-top: 0; + border-top: 1px solid #dbdbdb; +} +.avatar, .avatar-container { + float: left; + margin-right: 16px; + border-radius: 50%; + border: 1px solid #f5f5f5; +} +.s16.avatar, .s16.avatar-container { + width: 16px; + height: 16px; + margin-right: 8px; +} +.s18.avatar, .s18.avatar-container { + width: 18px; + height: 18px; + margin-right: 8px; +} +.s40.avatar, .s40.avatar-container { + width: 40px; + height: 40px; + margin-right: 8px; +} +.avatar { + transition-property: none; + width: 40px; + height: 40px; + padding: 0; + background: #fdfdfd; + overflow: hidden; + border-color: rgba(0, 0, 0, 0.1); +} +.avatar.center { + font-size: 14px; + line-height: 1.8em; + text-align: center; +} +.avatar.avatar-tile { + border-radius: 0; + border: 0; +} +.avatar-container { + overflow: hidden; + display: flex; +} +.avatar-container a { + width: 100%; + height: 100%; + display: flex; + text-decoration: none; +} +.avatar-container .avatar { + border-radius: 0; + border: 0; + height: auto; + width: 100%; + margin: 0; + align-self: center; +} +.avatar-container.s40 { + min-width: 40px; + min-height: 40px; +} +.rect-avatar { + border-radius: 2px; +} +.rect-avatar.s16 { + border-radius: 2px; +} +.rect-avatar.s18 { + border-radius: 2px; +} +.rect-avatar.s40 { + border-radius: 4px; +} +.tab-width-8 { + -moz-tab-size: 8; + tab-size: 8; +} +.gl-sr-only { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; +} +.gl-ml-3 { + margin-left: 0.5rem; +} +.content-wrapper > .alert-wrapper, +#content-body, .modal-dialog { + display: block; +} @import 'cloaking'; @include cloak-startup-scss(none); diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss new file mode 100644 index 00000000000..b3e53e35f6e --- /dev/null +++ b/app/assets/stylesheets/startup/startup-signin.scss @@ -0,0 +1,2409 @@ +@charset "UTF-8"; +*, +*::before, +*::after { + box-sizing: border-box; +} +html { + font-family: sans-serif; + line-height: 1.15; +} + header, nav, section { + display: block; +} +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #303030; + text-align: left; + background-color: #fff; +} +hr { + box-sizing: content-box; + height: 0; + overflow: visible; +} +h1, h2, h3 { + margin-top: 0; + margin-bottom: 0.25rem; +} +p { + margin-top: 0; + margin-bottom: 1rem; +} +address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; +} + +ul { + margin-top: 0; + margin-bottom: 1rem; +} + +ul ul { + margin-bottom: 0; +} + +strong { + font-weight: bolder; +} +sub { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} +sub { + bottom: -.25em; +} +a { + color: #007bff; + text-decoration: none; + background-color: transparent; +} +a:not([href]) { + color: inherit; + text-decoration: none; +} +pre, +code { + font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace; + font-size: 1em; +} +pre { + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; +} +img { + vertical-align: middle; + border-style: none; +} +svg { + overflow: hidden; + vertical-align: middle; +} +table { + border-collapse: collapse; +} +th { + text-align: inherit; +} +label { + display: inline-block; + margin-bottom: 0.5rem; +} +button { + border-radius: 0; +} +input, +button, +textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} +button, +input { + overflow: visible; +} +button { + text-transform: none; +} +button:not(:disabled), +[type="button"]:not(:disabled), +[type="reset"]:not(:disabled), +[type="submit"]:not(:disabled) { + cursor: pointer; +} +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + padding: 0; + border-style: none; +} + +input[type="checkbox"] { + box-sizing: border-box; + padding: 0; +} +textarea { + overflow: auto; + resize: vertical; +} +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} +[type="search"] { + outline-offset: -2px; +} +summary { + display: list-item; + cursor: pointer; +} +template { + display: none; +} +[hidden] { + display: none !important; +} +h1, h2, h3, +.h1, .h2, .h3 { + margin-bottom: 0.25rem; + font-weight: 600; + line-height: 1.2; + color: #303030; +} +h1, .h1 { + font-size: 2.1875rem; +} +h2, .h2 { + font-size: 1.75rem; +} +h3, .h3 { + font-size: 1.53125rem; +} +hr { + margin-top: 0.5rem; + margin-bottom: 0.5rem; + border: 0; + border-top: 1px solid rgba(0, 0, 0, 0.1); +} +.list-unstyled { + padding-left: 0; + list-style: none; +} +code { + font-size: 90%; + color: #1f1f1f; + word-wrap: break-word; +} +a > code { + color: inherit; +} +pre { + display: block; + font-size: 90%; + color: #303030; +} +pre code { + font-size: inherit; + color: inherit; + word-break: normal; +} +.container { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container { + max-width: 1140px; + } +} +.container-fluid { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container { + max-width: 1140px; + } +} +.row { + display: flex; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; +} + .col-sm-5, .col-sm-7, .col-sm-12 { + position: relative; + width: 100%; + padding-right: 15px; + padding-left: 15px; +} +.order-1 { + order: 1; +} +.order-12 { + order: 12; +} + +@media (min-width: 576px) { + .col-sm-5 { + flex: 0 0 41.66667%; + max-width: 41.66667%; + } + .col-sm-7 { + flex: 0 0 58.33333%; + max-width: 58.33333%; + } + .col-sm-12 { + flex: 0 0 100%; + max-width: 100%; + } + .order-sm-1 { + order: 1; + } + .order-sm-12 { + order: 12; + } +} +.table { + width: 100%; + margin-bottom: 0.5rem; + color: #303030; +} +.table th, +.table td { + padding: 0.75rem; + vertical-align: top; + border-top: 1px solid #dbdbdb; +} +.form-control, .search form { + display: block; + width: 100%; + height: 34px; + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + font-weight: 400; + line-height: 1.5; + color: #303030; + background-color: #fff; + background-clip: padding-box; + border: 1px solid #dbdbdb; + border-radius: 0.25rem; +} + +@media (prefers-reduced-motion: reduce) { +} +.form-control:-moz-focusring, .search form:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 #303030; +} +.form-control::placeholder, .search form::placeholder { + color: #5e5e5e; + opacity: 1; +} +.form-control:disabled, .search form:disabled { + background-color: #fafafa; + opacity: 1; +} +textarea.form-control { + height: auto; +} +.form-group { + margin-bottom: 1rem; +} +.form-inline { + display: flex; + flex-flow: row wrap; + align-items: center; +} + +@media (min-width: 576px) { + .form-inline label { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0; + } + .form-inline .form-group { + display: flex; + flex: 0 0 auto; + flex-flow: row wrap; + align-items: center; + margin-bottom: 0; + } + .form-inline .form-control, .form-inline .search form, .search .form-inline form { + display: inline-block; + width: auto; + vertical-align: middle; + } +} +.btn { + display: inline-block; + font-weight: 400; + color: #303030; + text-align: center; + vertical-align: middle; + cursor: pointer; + user-select: none; + background-color: transparent; + border: 1px solid transparent; + padding: 0.375rem 0.75rem; + font-size: 1rem; + line-height: 20px; + border-radius: 0.25rem; +} + +@media (prefers-reduced-motion: reduce) { +} +.btn.disabled, .btn:disabled { + opacity: 0.65; +} +a.btn.disabled, +fieldset:disabled a.btn { + pointer-events: none; +} +.btn-success { + color: #fff; + background-color: #108548; + border-color: #108548; +} +.btn-success.disabled, .btn-success:disabled { + color: #fff; + background-color: #108548; + border-color: #108548; +} +.btn-success:not(:disabled):not(.disabled):active, .btn-success:not(:disabled):not(.disabled).active, +.show > .btn-success.dropdown-menu-toggle { + color: #fff; + background-color: #0b572f; + border-color: #094c29; +} + .login-page input[type='submit'] { + display: block; + width: 100%; +} + .login-page input[type='submit'] + input[type='submit'] { + margin-top: 0.5rem; +} + .login-page input[type="submit"][type='submit'], +.login-page input[type="reset"][type='submit'], +.login-page input[type="button"][type='submit'] { + width: 100%; +} +.collapse:not(.show) { + display: none; +} + +.dropdown { + position: relative; +} + .dropdown-menu-toggle { + white-space: nowrap; +} + .dropdown-menu-toggle::after { + display: inline-block; + margin-left: 0.255em; + vertical-align: 0.255em; + content: ""; + border-top: 0.3em solid; + border-right: 0.3em solid transparent; + border-bottom: 0; + border-left: 0.3em solid transparent; +} + .dropdown-menu-toggle:empty::after { + margin-left: 0; +} +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 10rem; + padding: 0.5rem 0; + margin: 0.125rem 0 0; + font-size: 1rem; + color: #303030; + text-align: left; + list-style: none; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 0.25rem; +} +.dropdown-menu-right { + right: 0; + left: auto; +} + .divider { + height: 0; + margin: 4px 0; + overflow: hidden; + border-top: 1px solid #dbdbdb; +} +.dropdown-menu.show { + display: block; +} +.nav { + display: flex; + flex-wrap: wrap; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} +.nav-link { + display: block; + padding: 0.5rem 1rem; +} +.nav-link.disabled { + color: #5e5e5e; + pointer-events: none; + cursor: default; +} +.nav-tabs { + border-bottom: 1px solid #999; +} +.nav-tabs .nav-item { + margin-bottom: -1px; +} +.nav-tabs .nav-link { + border: 1px solid transparent; + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} +.nav-tabs .nav-link.disabled { + color: #5e5e5e; + background-color: transparent; + border-color: transparent; +} +.nav-tabs .nav-link.active, +.nav-tabs .nav-item.show .nav-link { + color: #525252; + background-color: #fff; + border-color: #999 #999 #fff; +} +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.tab-content > .tab-pane { + display: none; +} +.tab-content > .active { + display: block; +} +.navbar { + position: relative; + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + padding: 0.25rem 0.5rem; +} +.navbar .container, +.navbar .container-fluid { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; +} +.navbar-nav { + display: flex; + flex-direction: column; + padding-left: 0; + margin-bottom: 0; + list-style: none; +} +.navbar-nav .nav-link { + padding-right: 0; + padding-left: 0; +} +.navbar-nav .dropdown-menu { + position: static; + float: none; +} +.navbar-collapse { + flex-basis: 100%; + flex-grow: 1; + align-items: center; +} +.navbar-toggler { + padding: 0.25rem 0.75rem; + font-size: 1.25rem; + line-height: 1; + background-color: transparent; + border: 1px solid transparent; + border-radius: 0.25rem; +} + +@media (max-width: 575.98px) { + .navbar-expand-sm > .container, + .navbar-expand-sm > .container-fluid { + padding-right: 0; + padding-left: 0; + } +} + +@media (min-width: 576px) { + .navbar-expand-sm { + flex-flow: row nowrap; + justify-content: flex-start; + } + .navbar-expand-sm .navbar-nav { + flex-direction: row; + } + .navbar-expand-sm .navbar-nav .dropdown-menu { + position: absolute; + } + .navbar-expand-sm .navbar-nav .nav-link { + padding-right: 0.5rem; + padding-left: 0.5rem; + } + .navbar-expand-sm > .container, + .navbar-expand-sm > .container-fluid { + flex-wrap: nowrap; + } + .navbar-expand-sm .navbar-collapse { + display: flex !important; + flex-basis: auto; + } + .navbar-expand-sm .navbar-toggler { + display: none; + } +} +.card { + position: relative; + display: flex; + flex-direction: column; + min-width: 0; + word-wrap: break-word; + background-color: #fff; + background-clip: border-box; + border: 1px solid #dbdbdb; + border-radius: 0.25rem; +} +.card > hr { + margin-right: 0; + margin-left: 0; +} +.badge { + display: inline-block; + padding: 0.25em 0.4em; + font-size: 75%; + font-weight: 600; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25rem; +} + +@media (prefers-reduced-motion: reduce) { +} +.badge:empty { + display: none; +} +.btn .badge { + position: relative; + top: -1px; +} +.badge-pill { + padding-right: 0.6em; + padding-left: 0.6em; + border-radius: 10rem; +} +.media { + display: flex; + align-items: flex-start; +} +.close { + float: right; + font-size: 1.5rem; + font-weight: 600; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + opacity: .5; +} +button.close { + padding: 0; + background-color: transparent; + border: 0; + appearance: none; +} +a.close.disabled { + pointer-events: none; +} +.modal-dialog { + position: relative; + width: auto; + margin: 0.5rem; + pointer-events: none; +} + +@media (min-width: 576px) { + .modal-dialog { + max-width: 500px; + margin: 1.75rem auto; + } +} +.bg-transparent { + background-color: transparent !important; +} +.border { + border: 1px solid #dbdbdb !important; +} +.border-top { + border-top: 1px solid #dbdbdb !important; +} +.border-right { + border-right: 1px solid #dbdbdb !important; +} +.border-bottom { + border-bottom: 1px solid #dbdbdb !important; +} +.border-left { + border-left: 1px solid #dbdbdb !important; +} +.rounded { + border-radius: 0.25rem !important; +} +.clearfix::after { + display: block; + clear: both; + content: ""; +} +.d-none { + display: none !important; +} +.d-inline-block { + display: inline-block !important; +} +.d-block { + display: block !important; +} +.d-flex { + display: flex !important; +} + +@media (min-width: 576px) { + .d-sm-none { + display: none !important; + } +} + +@media (min-width: 768px) { + .d-md-block { + display: block !important; + } +} + +@media (min-width: 992px) { + .d-lg-none { + display: none !important; + } + .d-lg-block { + display: block !important; + } +} + +@media (min-width: 1200px) { + .d-xl-block { + display: block !important; + } +} +.flex-wrap { + flex-wrap: wrap !important; +} +.justify-content-between { + justify-content: space-between !important; +} +.align-items-center { + align-items: center !important; +} +.float-right { + float: right !important; +} +.fixed-top { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1030; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} +.mt-3 { + margin-top: 1rem !important; +} +.mb-3 { + margin-bottom: 1rem !important; +} +.m-auto { + margin: auto !important; +} + +@media (min-width: 576px) { + .mt-sm-0 { + margin-top: 0 !important; + } +} +.text-nowrap { + white-space: nowrap !important; +} +.text-left { + text-align: left !important; +} +.font-weight-normal { + font-weight: 400 !important; +} +.visible { + visibility: visible !important; +} +.form-control.focus, .search form.focus { + color: #303030; + background-color: #fff; + border-color: #80bdff; + outline: 0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} +input[type="color"].form-control { + height: 34px; + padding: 0.125rem 0.25rem; +} +input[type="color"].form-control:disabled { + background-color: #666; + opacity: 0.65; +} +.gl-badge { + display: inline-flex; + align-items: center; + font-size: 0.75rem; + font-weight: 400; + line-height: 1rem; + padding-top: 0.25rem; + padding-bottom: 0.25rem; + padding-left: 0.5rem; + padding-right: 0.5rem; + outline: none; +} +body, .form-control, .search form, +.search form { + font-size: 0.875rem; +} +button, +html [type='button'], +[type='reset'], +[type='submit'], +[role='button'] { + cursor: pointer; +} +h1, +.h1, +h2, +.h2, +h3, +.h3 { + margin-top: 20px; + margin-bottom: 10px; +} +input[type='file'] { + line-height: 1; +} + +strong { + font-weight: bold; +} +a { + color: #1068bf; +} +hr { + overflow: hidden; +} +code { + padding: 2px 4px; + color: #1f1f1f; + background-color: #f0f0f0; + border-radius: 4px; +} +.code > code { + background-color: inherit; + padding: unset; +} +table { + border-spacing: 0; +} +.hidden { + display: none !important; + visibility: hidden !important; +} +.hide { + display: none; +} + .dropdown-menu-toggle::after { + display: none; +} +.badge:not(.gl-badge), +.label { + padding: 4px 5px; + font-size: 12px; + font-style: normal; + font-weight: 400; + display: inline-block; +} +.nav-tabs { + border-bottom: 0; +} +.nav-tabs .nav-link { + border-top: 0; + border-left: 0; + border-right: 0; +} +.nav-tabs .nav-item { + margin-bottom: 0; +} +pre code { + white-space: pre-wrap; +} +input[type="color"].form-control { + height: 34px; +} +.toggle-sidebar-button .collapse-text, +.toggle-sidebar-button .icon-chevron-double-lg-left, +.toggle-sidebar-button .icon-chevron-double-lg-right { + color: #666; +} +svg { + vertical-align: baseline; +} +html { + overflow-y: scroll; +} +body { + text-decoration-skip: ink; +} +body.navless { + background-color: #fff !important; +} +.content-wrapper { + margin-top: 40px; + padding-bottom: 100px; +} +.container { + padding-top: 0; + z-index: 5; +} +.container .content { + margin: 0; +} + +@media (max-width: 575.98px) { + .container .content { + margin-top: 20px; + } +} + +@media (max-width: 575.98px) { + .container .container .title { + padding-left: 15px !important; + } +} +.navless-container { + margin-top: 40px; + padding-top: 32px; +} +.btn { + border-radius: 4px; + font-size: 0.875rem; + font-weight: 400; + padding: 6px 10px; + background-color: #fff; + border-color: #dbdbdb; + color: #303030; + color: #303030; + white-space: nowrap; +} +.btn:active, .btn.active { + box-shadow: rgba(0, 0, 0, 0.16); + background-color: #eaeaea; + border-color: #e3e3e3; + color: #303030; +} +.btn.btn-success, .btn.btn-register { + background-color: #108548; + border-color: #217645; + color: #fff; +} +.btn.btn-success:active, .btn.btn-success.active, .btn.btn-register:active, .btn.btn-register.active { + box-shadow: rgba(0, 0, 0, 0.16); + background-color: #24663b; + border-color: #0d532a; + color: #fff; +} +.btn svg { + height: 15px; + width: 15px; +} +.btn svg:not(:last-child), +.btn .fa:not(:last-child) { + margin-right: 5px; +} + .login-page input[type='submit'] { + width: 100%; + margin: 0; + margin-bottom: 15px; +} + .login-page input.btn[type='submit'] { + padding: 6px 0; +} +.badge.badge-pill:not(.gl-badge) { + font-weight: 400; + background-color: rgba(0, 0, 0, 0.07); + color: #525252; + vertical-align: baseline; +} +.hint { + font-style: italic; + color: #bfbfbf; +} +.bold { + font-weight: 600; +} +.tab-content { + overflow: visible; +} +pre.wrap { + word-break: break-word; + white-space: pre-wrap; +} +hr { + margin: 24px 0; + border-top: 1px solid #eee; +} +table a code { + position: relative; + top: -2px; + margin-right: 3px; +} +.loading { + margin: 20px auto; + height: 40px; + color: #525252; + font-size: 32px; + text-align: center; +} +.highlight { + text-shadow: none; +} +.chart { + overflow: hidden; + height: 220px; +} +.footer-links { + margin-bottom: 20px; +} +.footer-links a { + margin-right: 15px; +} +.break-word { + word-wrap: break-word; +} +.append-bottom-20 { + margin-bottom: 20px; +} +.center { + text-align: center; +} +.block { + display: block; +} +.flex { + display: flex; +} +.flex-grow { + flex-grow: 1; +} +.dropdown { + position: relative; +} +.show.dropdown .dropdown-menu { + transform: translateY(0); + display: block; + min-height: 40px; + max-height: 312px; + overflow-y: auto; +} + +@media (max-width: 575.98px) { + .show.dropdown .dropdown-menu { + width: 100%; + } +} + .show.dropdown .dropdown-menu-toggle, +.show.dropdown .dropdown-menu-toggle { + border-color: #c4c4c4; +} +.show.dropdown [data-toggle='dropdown'] { + outline: 0; +} +.search-input-container .dropdown-menu { + margin-top: 11px; +} + .dropdown-menu-toggle { + padding: 6px 8px 6px 10px; + background-color: #fff; + color: #303030; + font-size: 14px; + text-align: left; + border: 1px solid #dbdbdb; + border-radius: 0.25rem; + white-space: nowrap; +} + .no-outline.dropdown-menu-toggle { + outline: 0; +} + .dropdown-menu-toggle .fa { + color: #c4c4c4; +} +.dropdown-menu-toggle { + padding-right: 25px; + position: relative; + width: 160px; + text-overflow: ellipsis; + overflow: hidden; +} +.dropdown-menu-toggle .fa { + position: absolute; +} +.dropdown-menu { + display: none; + position: absolute; + width: auto; + top: 100%; + z-index: 300; + min-width: 240px; + max-width: 500px; + margin-top: 4px; + margin-bottom: 24px; + font-size: 14px; + font-weight: 400; + padding: 8px 0; + background-color: #fff; + border: 1px solid #dbdbdb; + border-radius: 0.25rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} +.dropdown-menu ul { + margin: 0; + padding: 0; +} +.dropdown-menu li { + display: block; + text-align: left; + list-style: none; + padding: 0 1px; +} +.dropdown-menu li > a, +.dropdown-menu li button { + background: transparent; + border: 0; + border-radius: 0; + box-shadow: none; + display: block; + font-weight: 400; + position: relative; + padding: 8px 12px; + color: #303030; + line-height: 16px; + white-space: normal; + overflow: hidden; + text-align: left; + width: 100%; +} +.dropdown-menu .divider { + height: 1px; + margin: 0.25rem 0; + padding: 0; + background-color: #dbdbdb; +} +.dropdown-menu .badge.badge-pill + span:not(.badge.badge-pill) { + margin-right: 40px; +} +.dropdown-select { + width: 300px; +} + +@media (max-width: 767.98px) { + .dropdown-select { + width: 100%; + } +} +.dropdown-content { + max-height: 252px; + overflow-y: auto; +} +.dropdown-loading { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + display: none; + z-index: 9; + background-color: rgba(255, 255, 255, 0.6); + font-size: 28px; +} +.dropdown-loading .fa { + position: absolute; + top: 50%; + left: 50%; + margin-top: -14px; + margin-left: -14px; +} + +@media (max-width: 575.98px) { + .navbar-gitlab li.dropdown { + position: static; + } + header.navbar-gitlab .dropdown .dropdown-menu { + width: 100%; + min-width: 100%; + } +} + +@media (max-width: 767.98px) { + .dropdown-menu-toggle { + width: 100%; + } +} +.flash-container { + margin: 0; + margin-bottom: 16px; + font-size: 14px; + position: relative; + z-index: 1; +} +.flash-container.sticky { + position: sticky; + position: -webkit-sticky; + top: 48px; + z-index: 251; +} +.flash-container.flash-container-page { + margin-bottom: 0; +} +.flash-container:empty { + margin: 0; +} +textarea { + resize: vertical; +} +input { + border-radius: 0.25rem; + color: #303030; + background-color: #fff; +} +label { + font-weight: 600; +} +label.label-bold { + font-weight: 600; +} +.form-control, .search form { + border-radius: 4px; + padding: 6px 10px; +} +.form-control::placeholder, .search form::placeholder { + color: #868686; +} +.gl-field-error { + color: #dd2b0e; + font-size: 0.875rem; +} +.gl-show-field-errors .form-control:not(textarea), .gl-show-field-errors .search form:not(textarea), .search .gl-show-field-errors form:not(textarea) { + height: 34px; +} +.gl-show-field-errors .gl-field-hint { + color: #303030; +} + +@media (max-width: 575.98px) { + .remember-me .remember-me-checkbox { + margin-top: 0; + } +} +body.ui-indigo .navbar-gitlab { + background-color: #292961; +} +body.ui-indigo .navbar-gitlab .navbar-collapse { + color: #d1d1f0; +} +body.ui-indigo .navbar-gitlab .container-fluid .navbar-toggler { + border-left: 1px solid #6868b9; +} +body.ui-indigo .navbar-gitlab .container-fluid .navbar-toggler svg { + fill: #d1d1f0; +} +body.ui-indigo .navbar-gitlab .navbar-sub-nav > li.active > a, +body.ui-indigo .navbar-gitlab .navbar-sub-nav > li.active > button, body.ui-indigo .navbar-gitlab .navbar-sub-nav > li.dropdown.show > a, +body.ui-indigo .navbar-gitlab .navbar-sub-nav > li.dropdown.show > button, +body.ui-indigo .navbar-gitlab .navbar-nav > li.active > a, +body.ui-indigo .navbar-gitlab .navbar-nav > li.active > button, +body.ui-indigo .navbar-gitlab .navbar-nav > li.dropdown.show > a, +body.ui-indigo .navbar-gitlab .navbar-nav > li.dropdown.show > button { + color: #292961; + background-color: #fff; +} +body.ui-indigo .navbar-gitlab .navbar-sub-nav { + color: #d1d1f0; +} +body.ui-indigo .navbar-gitlab .nav > li { + color: #d1d1f0; +} +body.ui-indigo .navbar-gitlab .nav > li > a.header-user-dropdown-toggle .header-user-avatar { + border-color: #d1d1f0; +} +body.ui-indigo .navbar-gitlab .nav > li.active > a, +body.ui-indigo .navbar-gitlab .nav > li.dropdown.show > a { + color: #292961; + background-color: #fff; +} +body.ui-indigo .search form { + background-color: rgba(209, 209, 240, 0.2); +} +body.ui-indigo .search .search-input::placeholder { + color: rgba(209, 209, 240, 0.8); +} +body.ui-indigo .search .search-input-wrap .search-icon, +body.ui-indigo .search .search-input-wrap .clear-icon { + fill: rgba(209, 209, 240, 0.8); +} +body.ui-indigo .nav-sidebar li.active { + box-shadow: inset 4px 0 0 #4b4ba3; +} +body.ui-indigo .nav-sidebar li.active > a { + color: #393982; +} +body.ui-indigo .nav-sidebar li.active .nav-icon-container svg { + fill: #393982; +} +body.ui-indigo .sidebar-top-level-items > li.active .badge.badge-pill { + color: #393982; +} +body.ui-indigo .nav-links li.active a, +body.ui-indigo .nav-links li a.active { + border-bottom: 2px solid #6666c4; +} +body.ui-indigo .nav-links li.active a .badge.badge-pill, +body.ui-indigo .nav-links li a.active .badge.badge-pill { + font-weight: 600; +} +.navbar-gitlab { + padding: 0 16px; + z-index: 1000; + margin-bottom: 0; + min-height: 40px; + border: 0; + border-bottom: 1px solid #dbdbdb; + position: fixed; + top: 0; + left: 0; + right: 0; + border-radius: 0; +} +.navbar-gitlab .logo-text { + line-height: initial; +} +.navbar-gitlab .logo-text svg { + width: 55px; + height: 14px; + margin: 0; + fill: #fff; +} +.navbar-gitlab .close-icon { + display: none; +} +.navbar-gitlab .header-content { + width: 100%; + display: flex; + justify-content: space-between; + position: relative; + min-height: 40px; + padding-left: 0; +} +.navbar-gitlab .header-content .title-container { + display: flex; + align-items: stretch; + flex: 1 1 auto; + padding-top: 0; + overflow: visible; +} +.navbar-gitlab .header-content .title { + padding-right: 0; + color: currentColor; + display: flex; + position: relative; + margin: 0; + font-size: 18px; + vertical-align: top; + white-space: nowrap; +} +.navbar-gitlab .header-content .title img { + height: 28px; +} +.navbar-gitlab .header-content .title img + .logo-text { + margin-left: 8px; +} +.navbar-gitlab .header-content .title.wrap { + white-space: normal; +} +.navbar-gitlab .header-content .title a { + display: flex; + align-items: center; + padding: 2px 8px; + margin: 5px 2px 5px -8px; + border-radius: 4px; +} +.navbar-gitlab .header-content .dropdown.open > a { + border-bottom-color: #fff; +} +.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) { + margin: 0 2px; +} +.navbar-gitlab .navbar-collapse { + flex: 0 0 auto; + border-top: 0; + padding: 0; +} + +@media (max-width: 575.98px) { + .navbar-gitlab .navbar-collapse { + flex: 1 1 auto; + } +} +.navbar-gitlab .navbar-collapse .nav { + flex-wrap: nowrap; +} + +@media (max-width: 575.98px) { + .navbar-gitlab .navbar-collapse .nav > li:not(.d-none) a { + margin-left: 0; + } +} +.navbar-gitlab .container-fluid { + padding: 0; +} +.navbar-gitlab .container-fluid .user-counter svg { + margin-right: 3px; +} +.navbar-gitlab .container-fluid .navbar-toggler { + position: relative; + right: -10px; + border-radius: 0; + min-width: 45px; + padding: 0; + margin: 8px -7px 8px 0; + font-size: 14px; + text-align: center; + color: currentColor; +} + +@media (max-width: 575.98px) { + .navbar-gitlab .container-fluid .navbar-nav { + display: flex; + padding-right: 10px; + flex-direction: row; + } +} +.navbar-gitlab .container-fluid .navbar-nav li .badge.badge-pill { + box-shadow: none; + font-weight: 600; +} + +@media (max-width: 575.98px) { + .navbar-gitlab .container-fluid .nav > li.header-user { + padding-left: 10px; + } +} +.navbar-gitlab .container-fluid .nav > li > a { + will-change: color; + margin: 4px 0; + padding: 6px 8px; + height: 32px; +} + +@media (max-width: 575.98px) { + .navbar-gitlab .container-fluid .nav > li > a { + padding: 0; + } +} +.navbar-gitlab .container-fluid .nav > li > a.header-user-dropdown-toggle { + margin-left: 2px; +} +.navbar-gitlab .container-fluid .nav > li > a.header-user-dropdown-toggle .header-user-avatar { + margin-right: 0; +} +.navbar-gitlab .container-fluid .nav > li .header-new-dropdown-toggle { + margin-right: 0; +} +.navbar-sub-nav > li > a, +.navbar-sub-nav > li > button, +.navbar-nav > li > a, +.navbar-nav > li > button { + display: flex; + align-items: center; + justify-content: center; + padding: 6px 8px; + margin: 4px 2px; + font-size: 12px; + color: currentColor; + border-radius: 4px; + height: 32px; + font-weight: 600; +} +.navbar-sub-nav > li > button, +.navbar-nav > li > button { + background: transparent; + border: 0; +} +.navbar-sub-nav .dropdown-menu, +.navbar-nav .dropdown-menu { + position: absolute; +} +.navbar-sub-nav { + display: flex; + margin: 0 0 0 6px; +} +.caret-down, +.btn .caret-down { + top: 0; + height: 11px; + width: 11px; + margin-left: 4px; + fill: currentColor; +} +.header-user .dropdown-menu, +.header-new .dropdown-menu { + margin-top: 4px; +} +.btn-sign-in { + background-color: #ebebfa; + color: #292961; + font-weight: 600; + line-height: 18px; + margin: 4px 0 4px 2px; +} +.title-container .badge.badge-pill, +.navbar-nav .badge.badge-pill { + position: inherit; + font-weight: 400; + margin-left: -6px; + font-size: 11px; + color: #fff; + padding: 0 5px; + line-height: 12px; + border-radius: 7px; + box-shadow: 0 1px 0 rgba(76, 78, 84, 0.2); +} +.title-container .badge.badge-pill.green-badge, +.navbar-nav .badge.badge-pill.green-badge { + background-color: #108548; +} +.title-container .badge.badge-pill.merge-requests-count, +.navbar-nav .badge.badge-pill.merge-requests-count { + background-color: #de7e00; +} +.title-container .badge.badge-pill.todos-count, +.navbar-nav .badge.badge-pill.todos-count { + background-color: #1f75cb; +} +.title-container .canary-badge .badge, +.navbar-nav .canary-badge .badge { + font-size: 12px; + line-height: 16px; + padding: 0 0.5rem; +} + +@media (max-width: 575.98px) { + .navbar-gitlab .container-fluid { + font-size: 18px; + } + .navbar-gitlab .container-fluid .navbar-nav { + table-layout: fixed; + width: 100%; + margin: 0; + text-align: right; + } + .navbar-gitlab .container-fluid .navbar-collapse { + margin-left: -8px; + margin-right: -10px; + } + .navbar-gitlab .container-fluid .navbar-collapse .nav > li:not(.d-none) { + flex: 1; + } + .header-user-dropdown-toggle { + text-align: center; + } + .header-user-avatar { + float: none; + } +} +.header-user.show .dropdown-menu { + margin-top: 4px; + color: #303030; + left: auto; + max-height: 445px; +} +.header-user.show .dropdown-menu svg { + vertical-align: text-top; +} +.header-user-avatar { + float: left; + margin-right: 5px; + border-radius: 50%; + border: 1px solid #f5f5f5; +} +.navbar-empty { + justify-content: center; + height: 40px; + background: #fff; + border-bottom: 1px solid #f0f0f0; +} + +@media (max-width: 575.98px) { + .nav-links > li > a .badge.badge-pill { + display: none; + } +} + +@media (max-width: 575.98px) { + .nav-links > li > a { + margin-right: 3px; + } +} +.media { + display: flex; + align-items: flex-start; +} +.card { + margin-bottom: 16px; +} +.nav-links:not(.quick-links) { + display: flex; + padding: 0; + margin: 0; + list-style: none; + height: auto; + border-bottom: 1px solid #dbdbdb; +} +.content-wrapper { + width: 100%; +} +.content-wrapper .container-fluid { + padding: 0 16px; +} + +@media (min-width: 768px) { + .page-with-contextual-sidebar { + padding-left: 50px; + } +} + +@media (min-width: 1200px) { + .page-with-contextual-sidebar { + padding-left: 220px; + } +} +.context-header { + position: relative; + margin-right: 2px; + width: 220px; +} +.context-header > a, +.context-header > button { + font-weight: 600; + display: flex; + width: 100%; + align-items: center; + padding: 10px 16px 10px 10px; + color: #303030; + background-color: transparent; + border: 0; + text-align: left; +} +.context-header .avatar-container { + flex: 0 0 40px; + background-color: #fff; +} +.context-header .sidebar-context-title { + overflow: hidden; + text-overflow: ellipsis; +} +.context-header .sidebar-context-title.text-secondary { + font-weight: normal; + font-size: 0.8em; +} +.nav-sidebar { + position: fixed; + z-index: 600; + width: 220px; + top: 40px; + bottom: 0; + left: 0; + background-color: #fafafa; + box-shadow: inset -1px 0 0 #dbdbdb; + transform: translate3d(0, 0, 0); +} + +@media (min-width: 576px) and (max-width: 576px) { + .nav-sidebar:not(.sidebar-collapsed-desktop) { + box-shadow: inset -1px 0 0 #dbdbdb, 2px 1px 3px rgba(0, 0, 0, 0.1); + } +} +.nav-sidebar.sidebar-collapsed-desktop { + width: 50px; +} +.nav-sidebar.sidebar-collapsed-desktop .nav-sidebar-inner-scroll { + overflow-x: hidden; +} +.nav-sidebar.sidebar-collapsed-desktop .badge.badge-pill:not(.fly-out-badge), +.nav-sidebar.sidebar-collapsed-desktop .sidebar-context-title, +.nav-sidebar.sidebar-collapsed-desktop .nav-item-name { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; +} +.nav-sidebar.sidebar-collapsed-desktop .sidebar-top-level-items > li > a { + min-height: 45px; +} +.nav-sidebar.sidebar-collapsed-desktop .fly-out-top-item { + display: block; +} +.nav-sidebar.sidebar-collapsed-desktop .avatar-container { + margin: 0 auto; +} +.nav-sidebar.sidebar-expanded-mobile { + left: 0; +} +.nav-sidebar a { + text-decoration: none; +} +.nav-sidebar ul { + padding-left: 0; + list-style: none; +} +.nav-sidebar li { + white-space: nowrap; +} +.nav-sidebar li a { + display: flex; + align-items: center; + padding: 12px 16px; + color: #666; +} +.nav-sidebar li .nav-item-name { + flex: 1; +} +.nav-sidebar li.active > a { + font-weight: 600; +} + +@media (max-width: 767.98px) { + .nav-sidebar { + left: -220px; + } +} +.nav-sidebar .nav-icon-container { + display: flex; + margin-right: 8px; +} +.nav-sidebar .fly-out-top-item { + display: none; +} +.nav-sidebar svg { + height: 16px; + width: 16px; +} + +@media (min-width: 768px) and (max-width: 1199px) { + .nav-sidebar:not(.sidebar-expanded-mobile) { + width: 50px; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .nav-sidebar-inner-scroll { + overflow-x: hidden; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .badge.badge-pill:not(.fly-out-badge), + .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-context-title, + .nav-sidebar:not(.sidebar-expanded-mobile) .nav-item-name { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items > li > a { + min-height: 45px; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .fly-out-top-item { + display: block; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .avatar-container { + margin: 0 auto; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .context-header { + height: 60px; + width: 50px; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .context-header a { + padding: 10px 4px; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items > li .sidebar-sub-level-items:not(.flyout-list) { + display: none; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .nav-icon-container { + margin-right: 0; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button { + padding: 16px; + width: 49px; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .collapse-text, + .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .icon-chevron-double-lg-left { + display: none; + } + .nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .icon-chevron-double-lg-right { + display: block; + margin: 0; + } +} +.nav-sidebar-inner-scroll { + height: 100%; + width: 100%; + overflow: auto; +} +.sidebar-sub-level-items { + display: none; + padding-bottom: 8px; +} +.sidebar-sub-level-items > li a { + padding: 8px 16px 8px 40px; +} +.sidebar-top-level-items { + margin-bottom: 60px; +} + +@media (min-width: 576px) { + .sidebar-top-level-items > li > a { + margin-right: 1px; + } +} +.sidebar-top-level-items > li .badge.badge-pill { + background-color: rgba(0, 0, 0, 0.08); + color: #666; +} +.sidebar-top-level-items > li.active { + background: rgba(0, 0, 0, 0.04); +} +.sidebar-top-level-items > li.active > a { + margin-left: 4px; + padding-left: 12px; +} +.sidebar-top-level-items > li.active .badge.badge-pill { + font-weight: 600; +} +.sidebar-top-level-items > li.active .sidebar-sub-level-items:not(.is-fly-out-only) { + display: block; +} +.toggle-sidebar-button, +.close-nav-button { + width: 219px; + position: fixed; + height: 48px; + bottom: 0; + padding: 0 16px; + background-color: #fafafa; + border: 0; + border-top: 1px solid #dbdbdb; + color: #666; + display: flex; + align-items: center; +} +.toggle-sidebar-button svg, +.close-nav-button svg { + margin-right: 8px; +} +.toggle-sidebar-button .icon-chevron-double-lg-right, +.close-nav-button .icon-chevron-double-lg-right { + display: none; +} +.collapse-text { + white-space: nowrap; + overflow: hidden; +} +.sidebar-collapsed-desktop .context-header { + height: 60px; + width: 50px; +} +.sidebar-collapsed-desktop .context-header a { + padding: 10px 4px; +} +.sidebar-collapsed-desktop .sidebar-top-level-items > li .sidebar-sub-level-items:not(.flyout-list) { + display: none; +} +.sidebar-collapsed-desktop .nav-icon-container { + margin-right: 0; +} +.sidebar-collapsed-desktop .toggle-sidebar-button { + padding: 16px; + width: 49px; +} +.sidebar-collapsed-desktop .toggle-sidebar-button .collapse-text, +.sidebar-collapsed-desktop .toggle-sidebar-button .icon-chevron-double-lg-left { + display: none; +} +.sidebar-collapsed-desktop .toggle-sidebar-button .icon-chevron-double-lg-right { + display: block; + margin: 0; +} +.fly-out-top-item > a { + display: flex; +} +.fly-out-top-item .fly-out-badge { + margin-left: 8px; +} +.fly-out-top-item-name { + flex: 1; +} +.close-nav-button { + display: none; +} + +@media (max-width: 767.98px) { + .close-nav-button { + display: flex; + } + .toggle-sidebar-button { + display: none; + } +} +table.table { + margin-bottom: 16px; +} +table.table .dropdown-menu a { + text-decoration: none; +} +table.table .success, +table.table .info { + color: #fff; +} +table.table .success a:not(.btn), +table.table .info a:not(.btn) { + text-decoration: underline; + color: #fff; +} +pre { + font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace; + display: block; + padding: 8px 12px; + margin: 0 0 8px; + font-size: 13px; + word-break: break-all; + word-wrap: break-word; + color: #303030; + background-color: #fafafa; + border: 1px solid #dbdbdb; + border-radius: 2px; +} +.monospace { + font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace; +} +input::-moz-placeholder, +textarea::-moz-placeholder { + color: #868686; + opacity: 1; +} +input::-ms-input-placeholder, +textarea::-ms-input-placeholder { + color: #868686; +} +input:-ms-input-placeholder, +textarea:-ms-input-placeholder { + color: #868686; +} +svg { + fill: currentColor; +} + +svg.s12 { + width: 12px; + height: 12px; +} + +svg.s16 { + width: 16px; + height: 16px; +} + +svg.s18 { + width: 18px; + height: 18px; +} + +svg.s12 { + vertical-align: -1px; +} + +svg.s16 { + vertical-align: -3px; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +table.code { + width: 100%; + font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace; + border: 0; + border-collapse: separate; + margin: 0; + padding: 0; + table-layout: fixed; + border-radius: 0 0 4px 4px; +} +.frame .badge.badge-pill { + position: absolute; + background-color: #428fdc; + color: #fff; + border: #fff 1px solid; + min-height: 16px; + padding: 5px 8px; + border-radius: 12px; +} +.frame .badge.badge-pill { + transform: translate(-50%, -50%); +} +.color-label { + padding: 0 0.5rem; + line-height: 16px; + border-radius: 100px; + color: #fff; +} +.label-link { + display: inline-flex; + vertical-align: text-bottom; +} +.label-link .label { + vertical-align: inherit; + font-size: 12px; +} +.login-page .container { + max-width: 960px; +} +.login-page .navbar-gitlab .container { + max-width: none; +} +.login-page .flash-container { + margin-bottom: 16px; +} +.login-page .brand-holder { + font-size: 18px; + line-height: 1.5; +} +.login-page .brand-holder p { + font-size: 16px; + color: #888; +} +.login-page .brand-holder h3 { + font-size: 22px; +} +.login-page .brand-holder img { + max-width: 100%; + margin-bottom: 30px; +} +.login-page .brand-holder a { + font-weight: 600; +} +.login-page p { + font-size: 13px; +} +.login-page .login-box, +.login-page .omniauth-container { + box-shadow: 0 0 0 1px #dbdbdb; + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; + padding: 15px; +} +.login-page .login-box .nav .active a, +.login-page .omniauth-container .nav .active a { + background: transparent; +} +.login-page .login-box .login-body, +.login-page .omniauth-container .login-body { + font-size: 13px; +} +.login-page .login-box .login-body input + p, +.login-page .login-box .login-body input ~ p.field-validation, +.login-page .omniauth-container .login-body input + p, +.login-page .omniauth-container .login-body input ~ p.field-validation { + margin-top: 5px; +} +.login-page .login-box .login-body .username .validation-success, +.login-page .omniauth-container .login-body .username .validation-success { + color: #217645; +} +.login-page .login-box .login-body .username .validation-error, +.login-page .omniauth-container .login-body .username .validation-error { + color: #dd2b0e; +} +.login-page .omniauth-container { + border-radius: 0.25rem; + font-size: 13px; +} +.login-page .omniauth-container p { + margin: 0; +} +.login-page .omniauth-container form { + width: 48%; + padding: 0; + border: 0; + background: none; + margin-bottom: 16px; +} + +@media (max-width: 991.98px) { + .login-page .omniauth-container form { + width: 100%; + } +} +.login-page .omniauth-container .omniauth-btn { + width: 100%; + padding: 8px; +} +.login-page .omniauth-container .omniauth-btn img { + width: 1.125rem; + height: 1.125rem; + margin-right: 16px; +} +.login-page .new-session-tabs { + display: flex; + box-shadow: 0 0 0 1px #dbdbdb; + border-top-right-radius: 4px; + border-top-left-radius: 4px; +} +.login-page .new-session-tabs li { + flex: 1; + text-align: center; + border-left: 1px solid #dbdbdb; +} +.login-page .new-session-tabs li:first-of-type { + border-left: 0; + border-top-left-radius: 4px; +} +.login-page .new-session-tabs li:last-of-type { + border-top-right-radius: 4px; +} +.login-page .new-session-tabs li:not(.active) { + background-color: #fafafa; +} +.login-page .new-session-tabs li a { + width: 100%; + font-size: 18px; +} +.login-page .new-session-tabs li.active > a { + cursor: default; +} +.login-page .submit-container { + margin-top: 16px; +} +.login-page input[type='submit'] { + margin-bottom: 0; +} +.login-page .devise-errors h2 { + margin-top: 0; + font-size: 14px; + color: #ae1800; +} +.devise-layout-html { + margin: 0; + padding: 0; + height: 100%; +} +.devise-layout-html body { + height: calc(100% - 51px); + margin: 0; + padding: 0; +} +.devise-layout-html body.navless { + height: calc(100% - 11px); +} +.devise-layout-html body .page-wrap { + min-height: 100%; + position: relative; +} +.devise-layout-html body .footer-container, +.devise-layout-html body hr.footer-fixed { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 40px; + background: #fff; +} +.devise-layout-html body .login-page-broadcast { + margin-top: 40px; +} +.devise-layout-html body .navless-container { + padding: 65px 15px; +} + +@media (max-width: 575.98px) { + .devise-layout-html body .navless-container { + padding: 0 15px 65px; + } +} +.milestones { + padding: 8px; + margin-top: 8px; + border-radius: 4px; + background-color: #dbdbdb; +} +.search { + margin: 0 8px; +} +.search form { + margin: 0; + padding: 4px; + width: 200px; + line-height: 24px; + height: 32px; + border: 0; + border-radius: 4px; +} + +@media (min-width: 1200px) { + .search form { + width: 320px; + } +} +.search .search-input { + border: 0; + font-size: 14px; + padding: 0 20px 0 0; + margin-left: 5px; + line-height: 25px; + width: 98%; + color: #fff; + background: none; +} +.search .search-input-container { + display: flex; + position: relative; +} +.search .search-input-wrap { + width: 100%; +} +.search .search-input-wrap .search-icon, +.search .search-input-wrap .clear-icon { + position: absolute; + right: 5px; + top: 4px; +} +.search .search-input-wrap .search-icon { + -moz-user-select: none; + user-select: none; +} +.search .search-input-wrap .clear-icon { + display: none; +} +.search .search-input-wrap .dropdown { + position: static; +} +.search .search-input-wrap .dropdown-menu { + left: -5px; + max-height: 400px; + overflow: auto; +} + +@media (min-width: 1200px) { + .search .search-input-wrap .dropdown-menu { + width: 320px; + } +} +.search .search-input-wrap .dropdown-content { + max-height: 382px; +} +.settings { + border-top: 1px solid #dbdbdb; +} +.settings:first-of-type { + margin-top: 10px; + border: 0; +} +.settings + div .settings:first-of-type { + margin-top: 0; + border-top: 1px solid #dbdbdb; +} +.avatar, .avatar-container { + float: left; + margin-right: 16px; + border-radius: 50%; + border: 1px solid #f5f5f5; +} +.s16.avatar, .s16.avatar-container { + width: 16px; + height: 16px; + margin-right: 8px; +} +.s18.avatar, .s18.avatar-container { + width: 18px; + height: 18px; + margin-right: 8px; +} +.s40.avatar, .s40.avatar-container { + width: 40px; + height: 40px; + margin-right: 8px; +} +.avatar { + transition-property: none; + width: 40px; + height: 40px; + padding: 0; + background: #fdfdfd; + overflow: hidden; + border-color: rgba(0, 0, 0, 0.1); +} +.avatar.center { + font-size: 14px; + line-height: 1.8em; + text-align: center; +} +.avatar.avatar-tile { + border-radius: 0; + border: 0; +} +.avatar-container { + overflow: hidden; + display: flex; +} +.avatar-container a { + width: 100%; + height: 100%; + display: flex; + text-decoration: none; +} +.avatar-container .avatar { + border-radius: 0; + border: 0; + height: auto; + width: 100%; + margin: 0; + align-self: center; +} +.avatar-container.s40 { + min-width: 40px; + min-height: 40px; +} +.rect-avatar { + border-radius: 2px; +} +.rect-avatar.s16 { + border-radius: 2px; +} +.rect-avatar.s18 { + border-radius: 2px; +} +.rect-avatar.s40 { + border-radius: 4px; +} +.tab-width-8 { + -moz-tab-size: 8; + tab-size: 8; +} +.gl-sr-only { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; + width: 1px; +} +.gl-mt-5 { + margin-top: 1rem; +} +.gl-ml-3 { + margin-left: 0.5rem; +} +.content-wrapper > .alert-wrapper, +#content-body, .modal-dialog { + display: block; +} +@import 'cloaking'; +@include cloak-startup-scss(none); diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss index e2b4d6b8e7a..bfbcb8c13c6 100644 --- a/app/assets/stylesheets/themes/_dark.scss +++ b/app/assets/stylesheets/themes/_dark.scss @@ -11,15 +11,6 @@ $gray-800: #f2f2f2; $gray-900: #fafafa; $gray-950: #fff; -$gl-gray-100: #333; -$gl-gray-200: #555; -$gl-gray-350: #666; -$gl-gray-400: #777; -$gl-gray-500: #999; -$gl-gray-600: #aaa; -$gl-gray-700: #ccc; -$gl-gray-800: #ddd; - $green-50: #072b15; $green-100: #0a4020; $green-200: #0e5a2d; @@ -94,6 +85,86 @@ $white-light: #2b2b2b; $white-normal: #333; $white-dark: #444; +$border-color: #4f4f4f; + +body.gl-dark { + --gray-10: #{$gray-10}; + --gray-50: #{$gray-50}; + --gray-100: #{$gray-100}; + --gray-200: #{$gray-200}; + --gray-300: #{$gray-300}; + --gray-400: #{$gray-400}; + --gray-500: #{$gray-500}; + --gray-600: #{$gray-600}; + --gray-700: #{$gray-700}; + --gray-800: #{$gray-800}; + --gray-900: #{$gray-900}; + --gray-950: #{$gray-950}; + + --green-50: #{$green-50}; + --green-100: #{$green-100}; + --green-200: #{$green-200}; + --green-300: #{$green-300}; + --green-400: #{$green-400}; + --green-500: #{$green-500}; + --green-600: #{$green-600}; + --green-700: #{$green-700}; + --green-800: #{$green-800}; + --green-900: #{$green-900}; + --green-950: #{$green-950}; + + --blue-50: #{$blue-50}; + --blue-100: #{$blue-100}; + --blue-200: #{$blue-200}; + --blue-300: #{$blue-300}; + --blue-400: #{$blue-400}; + --blue-500: #{$blue-500}; + --blue-600: #{$blue-600}; + --blue-700: #{$blue-700}; + --blue-800: #{$blue-800}; + --blue-900: #{$blue-900}; + --blue-950: #{$blue-950}; + + --orange-50: #{$orange-50}; + --orange-100: #{$orange-100}; + --orange-200: #{$orange-200}; + --orange-300: #{$orange-300}; + --orange-400: #{$orange-400}; + --orange-500: #{$orange-500}; + --orange-600: #{$orange-600}; + --orange-700: #{$orange-700}; + --orange-800: #{$orange-800}; + --orange-900: #{$orange-900}; + --orange-950: #{$orange-950}; + + --red-50: #{$red-50}; + --red-100: #{$red-100}; + --red-200: #{$red-200}; + --red-300: #{$red-300}; + --red-400: #{$red-400}; + --red-500: #{$red-500}; + --red-600: #{$red-600}; + --red-700: #{$red-700}; + --red-800: #{$red-800}; + --red-900: #{$red-900}; + --red-950: #{$red-950}; + + --indigo-50: #{$indigo-50}; + --indigo-100: #{$indigo-100}; + --indigo-200: #{$indigo-200}; + --indigo-300: #{$indigo-300}; + --indigo-400: #{$indigo-400}; + --indigo-500: #{$indigo-500}; + --indigo-600: #{$indigo-600}; + --indigo-700: #{$indigo-700}; + --indigo-800: #{$indigo-800}; + --indigo-900: #{$indigo-900}; + --indigo-950: #{$indigo-950}; + + --gl-text-color: #{$gray-900}; + --border-color: #{$border-color}; +} + $border-white-light: $gray-900; $border-white-normal: $gray-900; diff --git a/app/assets/stylesheets/themes/theme_blue.scss b/app/assets/stylesheets/themes/theme_blue.scss new file mode 100644 index 00000000000..9f9802f77f4 --- /dev/null +++ b/app/assets/stylesheets/themes/theme_blue.scss @@ -0,0 +1,14 @@ +@import './theme_helper'; + +body { + &.ui-blue { + @include gitlab-theme( + $theme-blue-200, + $theme-blue-500, + $theme-blue-700, + $theme-blue-800, + $theme-blue-900, + $white + ); + } +} diff --git a/app/assets/stylesheets/themes/theme_dark.scss b/app/assets/stylesheets/themes/theme_dark.scss new file mode 100644 index 00000000000..e6db6cd2a5e --- /dev/null +++ b/app/assets/stylesheets/themes/theme_dark.scss @@ -0,0 +1,14 @@ +@import './theme_helper'; + +body { + &.ui-dark { + @include gitlab-theme( + $gray-200, + $gray-300, + $gray-500, + $gray-700, + $gray-900, + $white + ); + } +} diff --git a/app/assets/stylesheets/themes/theme_green.scss b/app/assets/stylesheets/themes/theme_green.scss new file mode 100644 index 00000000000..6dcad6e1301 --- /dev/null +++ b/app/assets/stylesheets/themes/theme_green.scss @@ -0,0 +1,14 @@ +@import './theme_helper'; + +body { + &.ui-green { + @include gitlab-theme( + $theme-green-200, + $theme-green-500, + $theme-green-700, + $theme-green-800, + $theme-green-900, + $white + ); + } +} diff --git a/app/assets/stylesheets/themes/theme_helper.scss b/app/assets/stylesheets/themes/theme_helper.scss new file mode 100644 index 00000000000..85115cfd5d9 --- /dev/null +++ b/app/assets/stylesheets/themes/theme_helper.scss @@ -0,0 +1,204 @@ +@import '../page_bundles/mixins_and_variables_and_functions'; +/** + * Styles the GitLab application with a specific color theme + */ +@mixin gitlab-theme( + $search-and-nav-links, + $active-tab-border, + $border-and-box-shadow, + $sidebar-text, + $nav-svg-color, + $color-alternate +) { + // Header + + .navbar-gitlab { + background-color: $nav-svg-color; + + .navbar-collapse { + color: $search-and-nav-links; + } + + .container-fluid { + .navbar-toggler { + border-left: 1px solid lighten($border-and-box-shadow, 10%); + + svg { + fill: $search-and-nav-links; + } + } + } + + .navbar-sub-nav, + .navbar-nav { + > li { + > a, + > button { + &:hover, + &:focus { + background-color: rgba($search-and-nav-links, 0.2); + } + } + + &.active, + &.dropdown.show { + > a, + > button { + color: $nav-svg-color; + background-color: $color-alternate; + } + } + + &.line-separator { + border-left: 1px solid rgba($search-and-nav-links, 0.2); + } + } + } + + .navbar-sub-nav { + color: $search-and-nav-links; + } + + .nav { + > li { + color: $search-and-nav-links; + + > a { + &.header-user-dropdown-toggle { + .header-user-avatar { + border-color: $search-and-nav-links; + } + + .header-user-notification-dot { + border: 2px solid $nav-svg-color; + } + } + + &:hover, + &:focus { + @include media-breakpoint-up(sm) { + background-color: rgba($search-and-nav-links, 0.2); + } + + svg { + fill: currentColor; + } + + &.header-user-dropdown-toggle .header-user-notification-dot { + border-color: $nav-svg-color + 33; + } + } + } + + &.active > a, + &.dropdown.show > a { + color: $nav-svg-color; + background-color: $color-alternate; + + &:hover { + svg { + fill: $nav-svg-color; + } + } + + &.header-user-dropdown-toggle .header-user-notification-dot { + border-color: $white; + } + } + + .impersonated-user, + .impersonated-user:hover { + svg { + fill: $nav-svg-color; + } + } + } + } + } + + .navbar .title { + > a { + &:hover, + &:focus { + background-color: rgba($search-and-nav-links, 0.2); + } + } + } + + .search { + form { + background-color: rgba($search-and-nav-links, 0.2); + + &:hover { + background-color: rgba($search-and-nav-links, 0.3); + } + } + + .search-input::placeholder { + color: rgba($search-and-nav-links, 0.8); + } + + .search-input-wrap { + .search-icon, + .clear-icon { + fill: rgba($search-and-nav-links, 0.8); + } + } + + &.search-active { + form { + background-color: $white; + } + + .search-input-wrap { + .search-icon { + fill: rgba($search-and-nav-links, 0.8); + } + } + } + } + + // Sidebar + .nav-sidebar li.active { + box-shadow: inset 4px 0 0 $border-and-box-shadow; + + > a { + color: $sidebar-text; + } + + .nav-icon-container svg { + fill: $sidebar-text; + } + } + + .sidebar-top-level-items > li.active .badge.badge-pill { + color: $sidebar-text; + } + + .nav-links li { + &.active a, + &.md-header-tab.active button, + a.active { + border-bottom: 2px solid $active-tab-border; + + .badge.badge-pill { + font-weight: $gl-font-weight-bold; + } + } + } + + .branch-header-title { + color: $border-and-box-shadow; + } + + .ide-sidebar-link { + &.active { + color: $border-and-box-shadow; + box-shadow: inset 3px 0 $border-and-box-shadow; + + &.is-right { + box-shadow: inset -3px 0 $border-and-box-shadow; + } + } + } +} diff --git a/app/assets/stylesheets/themes/theme_indigo.scss b/app/assets/stylesheets/themes/theme_indigo.scss new file mode 100644 index 00000000000..bbf14afcca2 --- /dev/null +++ b/app/assets/stylesheets/themes/theme_indigo.scss @@ -0,0 +1,14 @@ +@import './theme_helper'; + +body { + &.ui-indigo { + @include gitlab-theme( + $indigo-200, + $indigo-500, + $indigo-700, + $indigo-800, + $indigo-900, + $white + ); + } +} diff --git a/app/assets/stylesheets/themes/theme_light.scss b/app/assets/stylesheets/themes/theme_light.scss new file mode 100644 index 00000000000..58003db4236 --- /dev/null +++ b/app/assets/stylesheets/themes/theme_light.scss @@ -0,0 +1,129 @@ +@import './theme_helper'; + +body { + &.ui-light { + @include gitlab-theme( + $gray-500, + $gray-700, + $gray-500, + $gray-500, + $gray-50, + $gray-500 + ); + + .navbar-gitlab { + background-color: $gray-50; + box-shadow: 0 1px 0 0 $border-color; + + .logo-text svg { + fill: $gray-900; + } + + .navbar-sub-nav, + .navbar-nav { + > li { + > a:hover, + > a:focus, + > button:hover { + color: $gray-900; + } + + &.active > a, + &.active > a:hover, + &.active > button { + color: $white; + } + } + } + + .container-fluid { + .navbar-toggler, + .navbar-toggler:hover { + color: $gray-500; + border-left: 1px solid $gray-100; + } + } + } + + .search { + form { + background-color: $white; + box-shadow: inset 0 0 0 1px $border-color; + + &:hover { + background-color: $white; + box-shadow: inset 0 0 0 1px $blue-200; + } + } + + .search-input-wrap { + .search-icon { + fill: $gray-100; + } + + .search-input { + color: $gl-text-color; + } + } + } + + .nav-sidebar li.active { + > a { + color: $gray-900; + } + + svg { + fill: $gray-900; + } + } + + .sidebar-top-level-items > li.active .badge.badge-pill { + color: $gray-900; + } + } + + &.gl-dark { + .logo-text svg { + fill: $gl-text-color; + } + + .navbar-gitlab { + background-color: $gray-50; + + .navbar-sub-nav, + .navbar-nav { + li { + > a:hover, + > a:focus, + > button:hover, + > button:focus { + color: $gl-text-color; + background-color: $gray-200; + } + } + + li.active, + li.dropdown.show { + > a, + > button { + color: $gl-text-color; + background-color: $gray-200; + } + } + } + + .search { + form { + background-color: $gray-100; + box-shadow: inset 0 0 0 1px $border-color; + + &:active, + &:hover { + background-color: $gray-100; + box-shadow: inset 0 0 0 1px $blue-200; + } + } + } + } + } +} diff --git a/app/assets/stylesheets/themes/theme_light_blue.scss b/app/assets/stylesheets/themes/theme_light_blue.scss new file mode 100644 index 00000000000..07d1c60a4c6 --- /dev/null +++ b/app/assets/stylesheets/themes/theme_light_blue.scss @@ -0,0 +1,14 @@ +@import './theme_helper'; + +body { + &.ui-light-blue { + @include gitlab-theme( + $theme-light-blue-200, + $theme-light-blue-500, + $theme-light-blue-500, + $theme-light-blue-700, + $theme-light-blue-700, + $white + ); + } +} diff --git a/app/assets/stylesheets/themes/theme_light_green.scss b/app/assets/stylesheets/themes/theme_light_green.scss new file mode 100644 index 00000000000..e122501b93c --- /dev/null +++ b/app/assets/stylesheets/themes/theme_light_green.scss @@ -0,0 +1,14 @@ +@import './theme_helper'; + +body { + &.ui-light-green { + @include gitlab-theme( + $theme-green-200, + $theme-green-500, + $theme-green-500, + $theme-light-green-700, + $theme-light-green-700, + $white + ); + } +} diff --git a/app/assets/stylesheets/themes/theme_light_indigo.scss b/app/assets/stylesheets/themes/theme_light_indigo.scss new file mode 100644 index 00000000000..5b607238ed9 --- /dev/null +++ b/app/assets/stylesheets/themes/theme_light_indigo.scss @@ -0,0 +1,14 @@ +@import './theme_helper'; + +body { + &.ui-light-indigo { + @include gitlab-theme( + $indigo-200, + $indigo-500, + $indigo-500, + $indigo-700, + $indigo-700, + $white + ); + } +} diff --git a/app/assets/stylesheets/themes/theme_light_red.scss b/app/assets/stylesheets/themes/theme_light_red.scss new file mode 100644 index 00000000000..fd3980183f3 --- /dev/null +++ b/app/assets/stylesheets/themes/theme_light_red.scss @@ -0,0 +1,14 @@ +@import './theme_helper'; + +body { + &.ui-light-red { + @include gitlab-theme( + $theme-light-red-200, + $theme-light-red-500, + $theme-light-red-500, + $theme-light-red-700, + $theme-light-red-700, + $white + ); + } +} diff --git a/app/assets/stylesheets/themes/theme_red.scss b/app/assets/stylesheets/themes/theme_red.scss new file mode 100644 index 00000000000..fa5ecc09f50 --- /dev/null +++ b/app/assets/stylesheets/themes/theme_red.scss @@ -0,0 +1,14 @@ +@import './theme_helper'; + +body { + &.ui-red { + @include gitlab-theme( + $theme-red-200, + $theme-red-500, + $theme-red-700, + $theme-red-800, + $theme-red-900, + $white + ); + } +} diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index 99a13cc4e44..9c666331c4f 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -112,10 +112,47 @@ top: 66vh; } -// Remove when https://gitlab.com/gitlab-org/gitlab-ui/-/issues/871 -// gets fixed on GitLab UI -.gl-sm-w-auto\! { +.gl-shadow-x0-y0-b3-s1-blue-500 { + box-shadow: inset 0 0 3px $gl-border-size-1 $blue-500; +} + + +.gl-sm-align-items-flex-end { + @media (min-width: $breakpoint-sm) { + align-items: flex-end; + } +} + +.gl-sm-text-body { + @media (min-width: $breakpoint-sm) { + color: $body-color; + } +} + +.gl-sm-font-weight-bold { @media (min-width: $breakpoint-sm) { + font-weight: $gl-font-weight-bold; + } +} + +.gl-min-h-6 { + min-height: $gl-spacing-scale-6; +} + +.gl-md-justify-content-end { + @media (min-width: $breakpoint-md) { width: auto !important; } } + +.gl-display-md-flex { + @media (min-width: $breakpoint-md) { + display: flex; + } +} + +.gl-display-md-none { + @media (min-width: $breakpoint-md) { + display: none; + } +} diff --git a/app/assets/stylesheets/vendors/atwho.scss b/app/assets/stylesheets/vendors/atwho.scss index f855c5c0d3d..f31dbbeafe8 100644 --- a/app/assets/stylesheets/vendors/atwho.scss +++ b/app/assets/stylesheets/vendors/atwho.scss @@ -24,8 +24,8 @@ .has-warning { .description { - color: $orange-700; - background-color: $orange-100; + color: $gray-900; + background-color: $orange-50; } } @@ -58,7 +58,7 @@ } &.has-warning { - color: $orange-700; + color: $orange-500; } } diff --git a/app/assets/stylesheets/vendors/tribute.scss b/app/assets/stylesheets/vendors/tribute.scss index 309cdf7245c..65f3d1b6199 100644 --- a/app/assets/stylesheets/vendors/tribute.scss +++ b/app/assets/stylesheets/vendors/tribute.scss @@ -1,6 +1,6 @@ .tribute-container { background: $white; - border: 1px solid $gl-gray-100; + border: 1px solid $gray-100; border-radius: $border-radius-base; box-shadow: 0 0 5px $issue-boards-card-shadow; color: $black; @@ -22,7 +22,7 @@ white-space: nowrap; small { - color: $gl-gray-500; + color: $gray-500; } &.highlight { |